Создать тему  Создать ответ 
Кусочки Haskell
19-03-2014, 17:18    
Сообщение: #1
arseniiv

± ∓
Сообщений: 227
Зарегистрирован: 05.07.12

Rainbow  
Так как написать нормально я не могу, я буду писать как попало, хотя и стараясь сделать это так, чтобы остальные могли выполнить такой код у себя. Надеюсь не использовать сразу же какие-нибудь модули не из пакета base без описания их установки.

∗ ∗ ∗ ∗ ∗

Для начала расскажу, как вообще можно использовать компилятор GHC (его можно найти как отдельно, так и в составе Haskell Platform). (Описание всего-всего есть в документации, но не лезть же в неё сразу.)

ghc --make имя-файла скомпилирует файл. По умолчанию не через си и не через промежуточный код LLVM, а прям так. Через си уже давно не советуют, а LLVM, говорят, может дать ускорение для чего-то, интенсивно использующего числа, но в любом случае компиляция через неё будет медленнее обычной.

ghc --interactive или ghci запустит REPL. Для виндовса есть ещё WinGHCi, оболочка над ghci.

В REPL’е можно как вводить разные объявления и выражения на вычисление, и смотреть, как компилятор ругается на их неправильность, так можно и менять режим работы, вводя разные :команды.

:set +m поставит многострочный режим ввода. Рекомендую делать это сразу.
:set +t начнёт выводить вместе со значениями выражений и их типы. Это может как помогать, так и запутывать, так что тут советовать не стану.
:type e, :t e, :kind t, :k t выведет тип выражения e или kind типа t.
:info имена покажет информацию об именах — разные определения и пр..
:module ± модули добавит или уберёт из контекста выполнения указанные модули. Поначалу загружен только модуль Prelude, но и от него можно избавиться.
:set также позволяет устанавливать некоторые параметры командной строки ghc. Например, так можно включать-отключать расширения языка: :set -XDeriveFunctor включит возможность автоматически определять экземпляр Functor a для нового типа a, :set -XNoDeriveFunctor выключит обратно. (Эту безобидную опцию выключать нет надобности, а другие может быть.)
:unset обращает действие :set, хотя мне всегда удавалось выключать включенное и с помощью второго — у опций обычно есть обратные варианты.
:? покажет справку по командам

Значение последнего вычисленного выражения ghci сохраняет в переменной it.

∗ ∗ ∗ ∗ ∗

Давайте начнём с программы, которая не делает ничего.

Точка входа в программу обозначается функцией main типа IO () — т. е. у неё есть побочный эффект, и при этом никакого осмысленного значения. Пока что я не помню, допустим ли тип IO Int и сделает ли он возвращённое число кодом возврата программы. Но программе, которая не делает ничего, нетривиальные коды возврата и не нужны. Итак,

module Main where

main = return ()


return просто «засовывает» аргумент в монаду. В какую монаду? Да в любую. Потому такой файл не скомпилируется, так как нужен конкретный тип, а не множество Monad m => m (). На самом деле — скомпилируется, потому что про Main.main GHC знает, что её тип должен быть IO ().

D:\___\bitsofhaskell>ghc --make -O VoidProgram.hs
[1 of 1] Compiling Main             ( VoidProgram.hs, VoidProgram.o )
Linking VoidProgram.exe ...


В результате у меня получилась 1,05-мегабайтная программа, которая ничего не делает. (Без оптимизации -O (есть ещё -O2) она занимает 1,06 MiB. Мило.)

Мы написали и скомпилировали пустую программу. :=

NOTA BENE. Ниже в теме было добавлено эссе на тему «Меня не устраивает большой размер GHC или Haskell Platform. Что делать?».

Honor thy error as a hidden intention
Вебсайт Найти все сообщения
Цитировать это сообщение
20-03-2014, 02:53    
Сообщение: #2
arseniiv

± ∓
Сообщений: 227
Зарегистрирован: 05.07.12

 
Теперь напишем программу, которая выводит переданные аргументы, а если их нет, ругается и посылает вызвавшему 13.

Заодно я познакомлю вас с другим оформлением исходного кода, называемом literate haskell. Файлы с таким кодом должны получать расширение .lhs, а каждая строка кода — начинаться с >. Зато комментарии-не-справа-от-кода не надо маркировать с помощью --. Потому в программы можно превращать всякие страницы и теховский код*. И сообщения на форумах, разумеется. В общем, можете брать весь текст (ну, без бб-кодов) этого поста и сохранять с указанным расширением. Ну и начнём код:

> module Main where

Нам понадобится импортировать три модуля:
  • System.Environment содержит функции работы с аргументами командной строки, именем программы и переменными окружения.
  • System.Exit содержит функцию выхода из программы с данным кодом и её специфические варианты. Хм.
  • Data.List содержит функции над списками. Некоторые из них переэкспортируются модулем по умолчанию Prelude, но не все.

Из первого нам понадобится только одна функция. Вот как это указывается:

> import System.Environment (getArgs)

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

Из второго нам не жалко импортировать всё:

> import System.Exit

Из третьего тоже не жалко, но не хочется, чтобы имена из него наложились на имена из Prelude (сейчас это искуственный пример, можно импортировать только нужную не имеющуюся в Prelude функцию, а ещё можно скрывать имена текущего контекста, приписывая hiding что скрывать). Можно написать вот как:

*> import qualified Data.List

и тогда придётся предварять импортированные имена с помощью Data.List. — неее… Хочется покороче. Вот так:

> import qualified Data.List as L

Теперь имена из этого модуля доступны с префиксом L..

Наша программа будет брать аргументы командной строки и выводить все вместе. Напишем сначала чистую функцию, которая берёт список и делает с ним что-то, превращая в строку. А потом будем передавать ей список аргументов.

> process :: [String] -> String
> process = concat . L.intersperse " & "


concat :: a -> [a] есть и в L, и в Prelude. Это специальный случай свёртки, начинающей с [] и ++ющей. И список строк превратится в карету одну большую конкатенацию.

L.intersperse :: a -> [a] -> [a]. intersperse elem list проставит elem между каждой парой соседних элементов list; например, intersperse 0 [1,2,3] = [1,0,2,0,3]. Это, конечно, тоже свёртка, но не такая явная, так что пусть живёт.

Итого, process ["cat","is","slon"] выльется в "cat & is & slon". Теперь надо соединить чистую идею с грязными помыслами. Нам нужны ещё три интересные функции и один не очень интересный тип:

getArgs :: IO [String] просто отдаёт аргументы командной строки. Ну, как сказать «отдаёт» — отдаёт, если согласимся на IO. Так что корректнее будет сказать «действие [см. ниже], отдающее аргументы…»; сама функция ничего не выводит, но её значение, будучи интерпретировано как-то тем, кто забирает к себе main, в которую это всё засунется, позволит ему достать аргументы и передать их куда-то.

putStrLn :: String -> IO () — это функция, которая по данной строке выдаёт действие, выводящее её в консоль, а потом ещё \n. Аналог без \nputStr. Обе переэкспортируются Prelude из System.IO.

data ExitCode = ExitSuccess | ExitFailure Int — это тип, описывающий код возврата. Не знаю, что, по их мнению, должно обозначать ExitFailure 0 — но раз уж разделили, разделили. И получаются значения ExitSuccess — всё хорошо и ExitFailure число — не всё хорошо с кодом.

exitWith :: ExitCode -> IO a — функция, выдающая нам по коду возврата действие, выполняющее выход с этим кодом.

Теперь код (другой):

> main = do args <- getArgs
>           if args == []
>             then do
>               putStrLn "Где мои аргументы?!"
>               exitWith $ ExitFailure 13
>             else do
>               let text = process args
>               putStrLn text


Вот вы видите эту do-нотацию и…

Да, это особый синтаксис записи выражений с монадами. Без него этот код можно было бы написать только как жуткую смесь всего вышеуказанного с операциями >>=, >>. Но и эта штука не самоочевидна — так с чего же начать?

А начну я с того, что main :: IO (), а тип IO a означает какое-то действие (в смысле, с возможными побочными эффектами), которое может нам отдать значение типа a, но взамен нам придётся операции с ним превратить тоже в действие. «Извлечь» это значение можно с помощью операции >>=, тип которой в нашем случае IO a -> (a -> IO b) -> IO b, второй аргумент — это действие, которому на вход поступает то «извлечённое» значение. А вот функций с типом IO a -> (a -> b) -> b или IO a -> a нет**, и потому от IO не избавиться. Хотя если у вас случайно получилось безобразие вида rrr :: IO (IO a), от него можно избавиться, сказав rrr >>= id, где id — тождественная функция, id = \x -> x. Магия вывода типов говорит, что у этой штуки будет тип IO a, хотя это ещё и не доказательство.

Так вот, есть у нас действие a1 — мы можем скомпоновать его с функцией f, выдающей другое действие, и получится действие a1 >>= f, которое выполнится так: сначала a1, потом извлекается его результат, передаётся в f, и получается действие a2, которое незамедлительно выполняется, конец. У такого выражения эквивалентная do-нотация будет такой:

do x <- a1
   f x


Читается так же прямо: «занеси результат действия a1 в x, а потом выполни то, что выдаст f x». Теперь прочитаем код main снова:

do args <- getArgs

«Прими же аргументы свыше,
[Обычная строка в do — значение из действия получает имя (и везде ниже оно будет доступно, если имя не переопределят).]

  if args == []

И если несть их,
[Внимание! Сравнение с пустым списком совершится за O(1), тогда как length args == 0 будет за O(n) — списки связные. Массивы есть отдельно.]

    then do

Тогда соверши сие:
[Это всё ещё часть if’а, и в каждой из альтернатив должно быть действие, потому вложенные do, т. к. на внутренности if’а и чего угодно синтаксис внешнего do не распространяется.]

      putStrLn "Где мои аргументы?!"

 Взмолись горе
  [Опять просто действие.]

      exitWith $ ExitFailure 13

 И уйди из этих краёв в отчаяньи, оставив камень
  [И снова действие.]

    else do

А иначе же, если аргументы пред тобой, соверши другое:
[Пошла вторая альтернатива. И опять вложенное do.]

      let text = process args

  Пошли аргументы свои, и получи ответ,
  [Nota bene! Это не действие и не обозначение результата действия именем — ничего не надо доставать из IO, есть обычная функция, легко отдающая свой результат. Просто посреди цепочки действий мы ввели ещё одно имя для чистого вычисления от известного значения другого. Как такое записывается без do, я потом скажу.]

      putStrLn text

  И высеки на камне его
  [И опять действие. Заметьте, последней строкой любого do должно быть действие без всяких связываний с именами; и тип этого действия будет типом всего блока do целиком.]

…и тут код заканчивается, и закрываются сразу два do. exitWith ExitSuccess, конечно, не нужен там, где он произойдёт сам собой.

* Потому что это другой возможный формат файла. Компиляторы обычно определяют, что там лежит. Если похоже на TeX, за код считаются куски между \begin{code} и \end{code}.
** Да есть, есть: unsafePerformIO :: IO a -> a, но название говорит само за себя…

Honor thy error as a hidden intention
Вебсайт Найти все сообщения
Цитировать это сообщение
20-03-2014, 19:57    
Сообщение: #3
arseniiv

± ∓
Сообщений: 227
Зарегистрирован: 05.07.12

 
Следующая программа будет вводить число из консоли (да, она заставит вас ввести именно число), а потом прибавит к нему 1 и выведет.

Как я когда-то говорил, в хаскеле есть два класса, связанные с конвертацией в строку и из — Show и Read. Самое основное в них — это методы

show :: Show a => a -> String, который, по-моему, следовало бы назвать toString (и класс — как-нибудь ToStringable), и
read :: Read a => String -> a с аналогичными замечаниями. Ладно, это не настолько страшно как то, что MonadPlus a не является экземпляром Alternative a. :rolleyes:

show и read (и несколько других более общих методов, учитывающих приоритет операций, чтобы не городить ненужные скобки), насколько мне известно, используются в GHCi: чтобы показывать значения — точно, а вот про чтение — не думаю. Читать приходится целые куски кода, одной read было бы мало (но вдруг где-нибудь и эта). Потому обычно предлагают определять их для своих типов так, чтобы они не только удовлетворяли уравнению read . show = id (полностью обратными они быть не всегда могут, y’know*), но и show x давала строку, которая, будучи понята как хаскельный код, вычисляющийся в x. Не всегда это уместно, и даже read после show может давать что-то не то, но для типов данных это, как правило, соблюдается. Например, множества из Data.Set выводятся в строку в виде fromList [элементы] — конструкторы типа Set не экспортируются; это часто делается, когда пользователю не должны быть доступны детали реализации. Вместо этого ему дают функции, которые называют smart constructors, по которым реализацию не узнать. Например, упомненная fromList имеет тип Ord a => [a] -> Set a (ограничение Ord a чуть-чуть намекает на реализацию, но ничего испортить всё равно не даст).

[Конструкторы данных — это функции, без использования которых нельзя создать значение соответствующего типа (они могут быть обёрнуты другими «нормальными» функциями, но без них не обойтись). Например, конструкторы типа Bool — это True :: Bool и False :: Bool, Maybe a — это Just :: a -> Maybe a и Nothing :: Maybe a, Char — все символьные литералы типа какого-нибудь 'c'. Конструкторы описываются при определении типа раз и навсегда, их нельзя пополнить, и начинаться они должны с заглавной буквы. И ещё пример: упомянутый выше тип ExitCode имеет конструкторы ExitSuccess :: ExitCode и ExitFailure :: Int -> ExitCode.]

Всё это я писал, чтобы обосновать, что нам сейчас хватит для чтения числа функции read, т. к. формат целых чисел хаскеля вполне пригоден для ввода с помощью человеков.

Только вот беда, если написать в интерпретаторе что-то такое:

ghci> read "abra cadabra" :: Int

мы получим ошибку (не исключение, несмотря на текст):

*** Exception: Prelude.read: no parse

Действительно, чего вы ещё хотели? read должен возвращать Int — там нет никакой возможности ни засунуть исключение (Int содержит только конечное число целых и ещё знаменитое значение ⊥, обозначающее ошибку, зацикливание или любой другой вариант отсутствия значения). Если указать тип Maybe Int, это тоже не поможет, т. к. тогда ожидаться будут строки вида "Just число" и "Nothing", но никак не числа с возможностью описать провал ввода результатом Nothing. Был бы смысл в функции readMaybe :: Read a => String -> Maybe a — и она есть в Text.Read. Но я про неё не знал и вообще хотел составить программу с обработкой исключения.

И вот, мы получаем ошибку, и она сразу завершает программу. Как этого избежать? Что же, теперь и списки на пустоту проверять перед вызовом head? (Она берёт первый элемент.) Ну да, лучше проверять или использовать может-быть-голову, потому что исключения ловить можно, из-за неопределённого порядка вычислений, только внутри IO, откуда уже не будет выхода.

Но мы всё-таки получаем ошибку, и она… в общем, «чистую» ошибку можно превратить в IO-исключение, передав в evaluate :: a -> IO a. Несмотря на совпадение типа с return, последний оставит ошибку ошибкой, и она вылезет за пределы обработчиков исключений и всё сломает, как и сейчас. Точное отличие между ними в том, что return не заставляет свой аргумент вычисляться, а evaluate — заставляет (заказывайте обзор способов ограничения лени в хаскеле).

Обрабатывать исключения можно по-разному, в том числе и двумя функциями из Control.Exceptionhandle и catch. Насколько я сейчас не уверен, это одно и то же с точностью до перестановки аргументов.

handle :: Exception e => (e -> IO a) -> IO a -> IO a
catch :: Exception e => IO a -> (e -> IO a) -> IO a

handle handler a выдаст действие, которое заключается в том, что выполняется действие a, но если там в середине возникает исключение e, немедленно происходит действие handler e. Обычный try-catch.

catch имеет тип, удобный для инфиксной записи и цепной записи: a `catch` handler1 `catch` handler2. Если исключение не обработано, оно будет вынесено дальше, и уж следующий-то catch его точно поймает!

Первая версия нашей программы будет сразу такая:

> module Main where

> import Control.Exception (handle, evaluate, ErrorCall)


ErrorCall — это тип исключения, в которое преобразуется ошибка, сделанная с помощью функции error :: String -> a. Эта функция используется примерно так:

head :: [a] -> a
head (x:_) = x
head []    = error "Prelude.head: empty list"


Так, дальше будем сверху вниз:

> main = do putStrLn "Введите целое число:"
>           n <- getInt
>           putStr "Если прибавить к нему 1, получится "
>           print (n + 1)


getInt будет иметь смысл «получи от пользователя число». print = putStrLn . show.

> getInt :: IO Int
> getInt = do putStr "> "
>             s <- getLine


getLine :: IO String получает строку текста (без \n) из консоли.

>             handle readErrorHandler $ evaluate (read s)

Это — последняя строка блока do, и имеет оно тип IO Int. Здесь уже не нужно указывать тип read s, который выведется в Int автоматически. Если, парся строку по принуждению evaluate, read поймёт, что его надули, он вызовет ошибку, а evaluate вместо этого выдаст такой IO Int, в котором вместо числа будет сидеть ErrorCall со строкой от errorа (здесь нам строка эта не понадобится, к слову), а handle это увидит (и всё-всё это будет выполнено при запуске main, не раньше) и выполнит действие обработчика, который…

> readErrorHandler :: ErrorCall -> IO Int
> readErrorHandler _ = do
>     putStrLn "По-твоему, это целое число? Давай другое!"
>     getInt

 
…который опять запустит getInt! Ну правильно, откуда ещё он может взять число? А ему нужно его взять! В принципе, мы могли бы устроить для getInt тип IO (Maybe Int), и обработчик мог бы быть таким:

*> readErrorHandler :: ErrorCall -> IO (Maybe Int)
*> readErrorHandler _ = do
*>     putStrLn "Эх ты, растяпа… Даже числа ввести не сумел(а|о)!"
*>     return Nothing


Но разве это интересно?

В итоге число когда-нибудь, по идее, случайно введётся, и программа прибавит к нему один и упокоится с миром. ◼**

А теперь скомпилируйте всё предыдущее и удивитесь: «> » будет выводиться уже после того как мы ввели число! Буфер вывода будет не вовремя вываливать своё содержимое. Решить эту проблему можно по-разному (см. как раз ссылку на StackOverflow), так что в результате трёхнедельных раздумий и возвращения назад во времени я выбрал такое решение (ниже начинается другой код, его не стоит компилировать вместе с предыдущим):

> module Main where

> import System.IO (hFlush, stdout)
> import Control.Exception (handle, evaluate, ErrorCall)

> readErrorHandler :: ErrorCall -> IO Int
> readErrorHandler _ = do
>     putStrLn "По-твоему, это целое число? Давай другое!"
>     getInt
 
> getInt :: IO Int
> getInt = do putStr "> "
>             hFlush stdout
>             s <- getLine
>             handle readErrorHandler $ evaluate (read s)

> main = do putStrLn "Введите целое число:"
>           n <- getInt
>           putStr "Если прибавить к нему 1, получится "
>           print (n + 1)


Думаю, семантика нововведений ясна, а stdout имеет тип Handle. К слову, putStrLn = hPutStrLn stdout. И теперь уже точно ◼!

Как обычно, если что-то непонятно (я многое не поясняю, надеясь на память из предыдущих разговоров, которых здесь рядом нет), спрашивайте, а то…

* Обязательно удостовертесь, что понимаете, почему!
** < квадрат < quadrad < QuEdDd.
*** Кстати, для записи рекурсивных действий есть синтаксическое расширение recursive do…

Honor thy error as a hidden intention
Вебсайт Найти все сообщения
Цитировать это сообщение
21-03-2014, 03:46    
Сообщение: #4
Agrest

井蛙 / жабенєтко в керниці
Сообщений: 1556
Зарегистрирован: 08.08.12

 
(19-03-2014 17:18)arseniiv писал(а):  return просто «засовывает» аргумент в монаду.
А что значит ()?

И да, я не очень разобрался во второй части... Можно ли попросить в дидактических целях переписать эту программу с горой и камнем с использованием >>=?
:blush:

«билингв мусорит в обоих языках — и первом, и втором» © Python
Вебсайт Найти все сообщения
Цитировать это сообщение
21-03-2014, 13:11    
Сообщение: #5
arseniiv

± ∓
Сообщений: 227
Зарегистрирован: 05.07.12

 
Конечно, можно. Будет щас.

Оригинал для сверки:

do
  args <- getArgs
  if args == []
    then do
      putStrLn "Где мои аргументы?!"
      exitWith $ ExitFailure 13
    else do
      let text = process args
      putStrLn text


Удаление сахара приведёт к

getArgs >>= (\args ->
  if args == []
    then
      putStrLn "Где мои аргументы?!" >>
      exitWith (ExitFailure 13)
    else
      let text = process args in
        putStrLn text)


(Тут можно убрать сразу все отступы, они просто для удобства чтения и сопоставления.)

Всего два bind’а, неплохо! (bind — это название >>=, а вот про >> не помню).

a >> b означает действие «сначала a, потом b», в отличие от a >>= f, где f — функция, строящая следующее действие на основании значения, извлечённого из a.

(>>) :: IO a -> IO b -> IO b
a1 >> a2 = a1 >>= \_ -> a2


(21-03-2014 03:46)Agrest писал(а):  И да, я не очень разобрался во второй части...
К слову, я и не ожидал, что все сразу начнут понимать do, IO и монады, потому что далеко не до конца дописал про них, но это в один раз не пишется. Видимо. Насколько убеждают остальные руководства. :)

Honor thy error as a hidden intention
Вебсайт Найти все сообщения
Цитировать это сообщение
21-03-2014, 13:36    
Сообщение: #6
arseniiv

± ∓
Сообщений: 227
Зарегистрирован: 05.07.12

 
(21-03-2014 03:46)Agrest писал(а):  А что значит ()?
Это единственное значение типа (). В чистых функциях оно особо не нужно — от чистой функции нужно её значение, и если значением будет () и больше заведомо ничего (ну кроме ошибки ⊥*, но тогда всё полетит, если захочешь узнать, что же там вернули), то функция не даёт нисколько информации и бесполезна. А вот для функции с побочным эффектом это уже становится полезным: putStr получает строку, выводит её… а никакого значения назад интересного вернуть не может — не возвращать же эту строку назад? (можно определить другую putStr', чтобы возвращала, конечно!), вот и приходится возвращать (), т. к. это лучшее из всего, что можно проигнорировать. Ну и «в исполнении» IO это получится IO (), и слева ещё приделаем принимаемую строку, готово.

Лучшее, несмотря на то, что мы можем легко создать себе сколько угодно типов с одним конструктором без параметров:

data One = Co -- Co :: One
data Oneone = CoCo -- CoCo :: Oneone


Потому что он уже есть с самого начала. :???

Ещё можно с помощью списков и () представлять числа: [] — 0, [()] — 1, [(),()] — 2… [(),()] ++ [(),(),()] = [(),(),(),(),()]; [(),()] >> [(),(),()] = [(),(),(),(),(),()]! (Хотя тут монадного экземпляра списка не нужно, но без подключения других модулей он есть сразу.)

Это, конечно, я шучу.

* ⊥ впрямую (чтобы можно было матчить) не выразимо на хаскеле из-за проблемы останова (если можно было бы всегда узнать, что функция дала ⊥…), хотя получить его можно с помощью undefined :: a, например. Или просто недоопределить функцию:

f :: Int -> ()
f 42 = ()


или определить похуже:

g :: Bool
g = g -- или g = not g


и вызвать f 5 или g.

Иногда это даже бывает полезно для проверки (не)ленивости в нужном аргументе функции.

P. S. Переделанный код будет сообщением выше.

Honor thy error as a hidden intention
Вебсайт Найти все сообщения
Цитировать это сообщение
21-03-2014, 15:39    
Сообщение: #7
arseniiv

± ∓
Сообщений: 227
Зарегистрирован: 05.07.12

 
А здесь не будет программы. Здесь я опишу небольшой кусок Prelude подробно.

Буль-буль

Как уже писал, у Bool два конструктора, True и False, и на них мы можем матчить, так что могли бы и сами определить операции &&, || и функцию not как-то так:

(&&) :: Bool -> Bool -> Bool
False && _ = False
True && a = a

(||) :: Bool -> Bool -> Bool
False && _ = a
True || _ = True

not :: Bool -> Bool
not True = False
not False = True


Ещё немного о классах

Bool входит в несколько экземпляров классов, и это тот способ, с помощью которого я коснусь сейчас их смысла:

Eq Bool — значения можно сравнивать на равенство с помощью == и /=.

Ord Bool — значения линейно упорядочены, и можно использовать <, <=, >, >=. Есть ещё две бинарные функции max и min и редко используемая функция compare, возвращающая значение типа OrderingLT, EQ или GT. Для определений порядка на своих типах она полезна.

Здесь True > False

Read Bool, Show Bool — как уже описано выше, говорят, что можно взаимно конвертировать значения Bool и String.

Bounded Bool — есть самое большое из значений maxBound и самое маленькое minBound.

Enum Bool — значения можно конвертировать в целые числа и обратно с помощью fromEnum :: Enum a => a -> Int и toEnum :: Enum a => Int -> a.

fromEnum False = 0, fromEnum True = 1.

Ещё Enum определяет методы получения предыдущего или следующего значения pred :: Enum a => a -> a и succ :: Enum a => a -> a, например:

ghci> succ 'h'
'i'
ghci> pred (minBound :: Bool)
*** Exception: Prelude.Enum.Bool.pred: bad argument


И ещё: так же как монады имеют синтаксический сахар в виде блоков do, с экземплярами Enum связан особый синтаксис записи списков, целых четыре:

[m ..] = enumFrom m

даст список, начинающийся с m, в котором следующие элементы больше на 1, 2, 3 и т. д.. Он может оказаться и бесконечным.

[m .. n] = enumFromTo m n

даст список, заканчивающийся на элементе, не большем n.

[m, r ..] = enumFromThen m r

аналогично первому, но промежутком между элементами будут не единицы, а «разность» между r и m. Она может быть и нулём, и отрицательным числом, и даже нецелой, и не иметь в последнем случае никакой связи со значениями fromEnum от m и r*. Видимо, из-за такой непрочной связи класс Enum стоит в будущем разбить на несколько.

[m, r .. n] = enumFromThenTo m r n

сочетает в себе преимущества и недостатки второго и третьего вариантов.

Экземпляры перечисленных шести классов для какого-нибудь своего нового типа можно записать механически, исходя из его определения (если не нужно что-то особое). Потому, чтобы не мучить людей, можно поручить это компилятору, после data ... = ... написав deriving (Eq, Ord, Show) или, например, deriving Enum (т. е. для одного класса без скобок).

Характер

Хочу поскорей покончить с описанием типа символов. Это Char. Он может содержать любой один символ уникода, при этом fromEnum и toEnum связывают символы и их коды, так что можно гарантировать, что fromEnum 'A' = 65 (неужели помню? :o), но всё равно пользуйтесь для прозрачности методами Enum. Ну и, разумеется, String — это синоним [Char], и вот как это объявить:

type String = [Char]

Всё, всё, довольно. 1114112 — это много, слишком много…

Скобочки

У единичного типа есть братья-кортежи, которые намного интереснее, и ещё в лучших традициях математики нет брата-кортежа из одного элемента.

Типы кортежей, как и тип списков, можно обозначать некрасиво, но справедливо, и красиво, но сладко:
  • Справедливо: (,) Int Char, (,,) Bool Bool Bool, (,,,) a b c d. Аналогия — [] Char. Конструктор типа спереди, типы-аргументы после — как и с другими типами, конструкторы которых по случайности пишутся буквами: IO Int, например.
  • Сладко: (Int, Char), (Bool, Bool, Bool), (a, b, c, d). Аналогия — [Char].

Даже интерпретатор будет пользоваться вторым способом.

Практически никаких особых функций для кортежей, не связанных с какими-то другими особыми типами, нет. Типу (,) a b повезло немного больше, чем остальным: есть две функции-проекции

fst :: (a, b) -> a
fst (a, _) = a

snd :: (a, b) -> b
snd (_, b) = b


Остальные проекции можно сляпать и на коленке вот так: \(_, _, a, _) -> a. (Как это «ты не говорил про шаблоны вместо параметра в лямбда-выражении»?)

В категорном смысле кортеж — это тип-произведение (а () — это терминальный объект, выступающий для произведения единицей). С произведением типов связано произведение морфизмов (которые здесь — функции), которое придётся написать самим:

(⊗) :: (a -> a') -> (b -> b') -> (a, b) -> (a', b')
(f ⊗ g) (a, b) = (f a, g b)


Есть ещё его урезанный вариант

(∧) :: (a -> b) -> (a -> c) -> a -> (b, c)
(f ∧ g) a = (f a, g a)


(Для приложений им надо будет приделать ещё левоассоциативность и приоритет, но не хочется смотреть, какой из десяти.) Кстати, ещё есть

curry :: ((a, b) -> c) -> a -> b -> c
curry f a b = f (a, b)

uncurry :: (a -> b -> c) -> (a, b) -> c
uncurry f (a, b) = f a b


И снова остальные типы кортежей обделены.

Ещё можно написать функции с типами (a -> a') -> (a, b) -> (a', b) и (b -> b') -> (a, b) -> (a, b'). Что они могли бы делать? Кроме того, ну почти естественна ещё и функция-селектор из типа с n значениями и n-элементного кортежа. Для пары её можно написать как-то так:

uncurry . (\a b c -> if a then b else c)

или так:

\a -> if a then fst else snd

Выведите её тип и напишите, что именно с ней не в порядке!** :D

Смысловое: типы кортежей стоит использовать, например, для возврата нескольких значений (когда есть причины вернуть их из одной функции, а не каждое из своей), и если значения не связаны друг с другом так, что для них есть смысл наопределять функций и представить каким-нибудь новым типом данных с объявлением data Date = Date Int Month Int (обычно, если конструктор данных один и экспортируется из модуля, его называют так же как конструктор типа — они всё равно находятся в разных пространствах имён, а запоминать-выдумывать меньше). К тому же, свой тип можно объявить и так:

data Date = Date { year :: Int, month :: Month, day :: Int }

При этом, кроме обычных вещей, автоматически определятся функции year :: Date -> Int, month :: Date -> Month и day :: Date -> Int, а ещё можно будет описывать значения этого типа вот так: Date { year = 2014, month = March, day = 23 }, матчить вот так: Date { y, m, d } и — внимание! — модифицировать значение вот так: myDate { year = 1985 }. Последнее эквивалентно (\(Date d m y) -> Date d m 1985) myDate.

Левое и правое

Посмотрев на типы-произведение, логично бы сразу рассмотреть стандартный тип-сумму. Он всего один — Either a b. Ну, точнее, их столько же, сколько (a, b) — много-много, но конструктор типа всего один.

Either a b имеет два конструктора данных Left :: a -> Either a b и Right :: b -> Either a b, так что его значения — это дизъюнктное объединение значений типа a и типа b. А можно ли определить сумму функций? Да:

(⊕) :: (a -> a') -> (b -> b') -> Either a b -> Either a' b'
(f ⊕ g) Left a = Left $ f a
(f ⊕ g) Right b = Right $ g b


И урезанный вариант:

(∨) :: (a -> c) -> (b -> c) -> Either a b -> c
(f ∨ g) Left a = f a
(f ∨ g) Left b = g b


…и этот вариант из всех четырёх ⊕, ⊗, ∧, ∨ был выбран находиться в Prelude под именем either. Подобная (но ничуть не аналогичная) функция есть, чтобы разобрать Maybe, и называется она угадайте как (или не стоит: она будет описана ниже).

Как и Maybe v, Either err v используется подчас при столкновении с ошибками, только вместо безликого Nothing у нас появляется Left сообщение, куда мы можем записать нужную информацию (но лучше всё-таки Right answer). Если она есть — например, ошибка при взятии головы списка может быть только одна — пустота, так что Either для описания результата подобной функции будет совершенно лишним.

А в Data.Either есть ещё три функции:

rights :: [Either a b] -> [​b] — отделить все вершки,
lefts :: [Either a b] -> [a] — отделить все корешки,
partitionEithers :: [Either a b] -> ([a], [​b]) — эти туда, те сюда (да нет же, наоборот!).

Эти выглядят на первый взгляд более практичными, но почему не переэкспортированы?..

Может быть

Уже много раз упоминавшийся тип Maybe… Чтобы работать с его значениями эффективнее, есть немало готовых функций. В первую очередь, это

maybe :: b -> (a -> b) -> Maybe a -> b
maybe d _ Nothing = d
maybe _ f (Just x) = f x


Она обработает переданное может-быть-значение функцией или вернёт нам деньги значение по умолчанию, переданное в самом начале. А вообще, лучше просто использовать то, что Maybe — монада, но о монадах будет не сейчас.

Остальные функции придётся тащить из Data.Maybe. Наиболее интересные из них — это

catMaybes :: [Maybe a] -> [a] — удаляет Nothingи из списка, а остальные за ненадобностью разворачивает (catMaybes [Just 2, Nothing, Just 3] = [2, 3]) и

mapMaybe :: (a -> Maybe b) -> [a] -> [​b]
mapMaybe = catMaybes . map


Списки

Есть два конструктора у списков,
Один из них зовётся [] :: [a] -- nil,
Другой из них зовётся (:​) :: a -> [a] -> [a] -- cons,
На том и кончится анонс.

Нет, вру, не кончится. Перед тем как погружаться в пучину (Functor, Applicative, Alternative, Monad, Monoid, Foldable, Traversable, Arrow, …), было бы крайне дурным тоном не рассмотреть конкретные вещи.

Начнём с нашей несравненной map:

map :: (a -> b) -> [a] -> [​b]
map _ [] = []
map f (x:xs) = f x : map f xs


map можно даже рассматривать как вид свёртки, но я не буду (потому что не помню как :angel:​). Ещё можно упомянуть, что многие-многие хорошие вещи имеют тип (a -> b) -> (f a -> f b), и зовут их эндофункторами молчу, молчу.

(++) :: [a] -> [a] -> [a] — конкатенация списков.

filter :: (a -> Bool) -> a -> a оставит в списке только элементы, удовлетворяющие предикату-первому-параметру.

null :: [a] -> Bool проверяет список на пустоту.

head, last :: [a] -> a и tail, init :: [a] -> [a] — частичные функции и не работают на пустом списке, и потому из лучших побуждений я не стану говорить, что они делают!!!

length :: [a] -> Int работает за O(n), и я снова не сказал бы, что она делает, но другого способа узнать длину односвязного списка в природе не существует…

(!!) :: [a] -> Int -> a — 0-based индексирование. xs !! n даст n-й элемент, если он есть, за O(n). Нет, списки для этого не предназначены. Есть Arrayи и всякие Setы-Mapы. Осторожно, ведь эта функция тоже частичная.

reverse :: [a] -> [a] поиграет с пользователем в реверси. (Внутри используется unsafePerformIO, и потому тип результата — не IO [a].) Если список бесконечный, игра никогда не закончится.

Остальное можно прочитать здесь, а я лучше добавлю про list comprehensions.

[ f a | a <- as ] = map f as = map (\a -> f a) as
[ f a b | a <- as, b <- bs ] = concatMap (\a -> map (f a) bs) as
[ f a | a <- as, p a ] = filter p $ map f as
[ f a b c | a <- as, b <- bs, p a b, q a, c <- cs, r c b ] = wtf' -- зачем они тогда были бы нужны?


Если использовать расширения языка ParallelListComp и TransformListComp, синтаксис этих штук обогатится. Можно будет написать такое:

ghci> :set -XParallelListComp
ghci> [ (a,b) | a <- [1,3,5] | b <- [2,4..] ]
[(1,2),(3,4),(5,6)]


и, со вторым расширением, SQL-подобные «запросы».

Кстати, список — тоже монада, и оччень забавная.

Числа и их классы — тема настолько запутанная, что лучше её обсуждать отдельно

P. S. Кто не боится переполнения, вот.

* Смотрите: fromEnum 2.1 = fromEnum 2.9 = 2, но при этом [2.1, 2.2 .. 2.9] срабатывает и даёт (ну почти что) [2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9].

** А ещё напишите как можно короче выражение с типом a -> b -> c -> d -> (a, b, c, d). Надеюсь, мне не придётся писать, во сколько символов можно уложиться? ;)

Honor thy error as a hidden intention
Вебсайт Найти все сообщения
Цитировать это сообщение
27-03-2014, 21:30    
Сообщение: #8
arseniiv

± ∓
Сообщений: 227
Зарегистрирован: 05.07.12

RE: Кусочки Haskell
Следующая программа сочтёт в себе случайное и не совсем случайное. Она будет получать аргумент-число, говорящее, строку из скольки случайных символов нужно составить. Алфавит будет браться из файла alphabet.txt в папке программы, а строка будет писаться в result.txt там же; оба файла в UTF-8.

Пока хорошенько работать с параметрами командной строки нам не приходится, так что, чтобы не забыть, оставлю здесь ссылку на System.Console.GetOpt из пакета base.

Исходя из описания, первая итерация будет такой:

module Main where

import System.Environment (getArgs)
import System.Exit

main :: IO ()
main = do
  args <- getArgs
  case extractLength args of -- «портативное» сопоставление с шаблонами
    Nothing  -> exitWith incorrectArgs
    Just len -> workWith len


Код возврата, соответствующий плохим аргументам:

incorrectArgs :: ExitCode
incorrectArgs = ExitFailure 1


Превратить аргументы в верное значение длины строки или выдать Nothing:

extractLength :: [String] -> Maybe Int
extractLength = undefined -- а не знаем пока, как


Следующая функция с нами, чтобы не писать остаток main с тройным отступом.
У неё можно и смысл даже найти, хотя можно было бы избавиться от отступа по-другому. Например, кинуть исключение. Не исключено, что дальше это будет исполнено.

workWith :: Int -> IO ()
workWith len = do
  alphabet <- readAlphabet
  word <- generateWord len alphabet
  save word


Новые имена продолжают всплывать из небытия, что ж. Следующее действие должно бы нам прочитать алфавит из файла:

readAlphabet :: IO [Char]
readAlphabet = undefined


Другое — сгенерировать случайное слово указанной длины и в указанном алфавите, которые к этому моменту мы имеем:

generateWord :: Int -> [Char] -> IO String
generateWord = undefined


И третье должно бы сохранить это слово в файле:

save :: String -> IO ()
save = undefined


Вторая итерация будет завтра.

Honor thy error as a hidden intention
Вебсайт Найти все сообщения
Цитировать это сообщение
28-03-2014, 20:13    
Сообщение: #9
arseniiv

± ∓
Сообщений: 227
Зарегистрирован: 05.07.12

 
Начнём с простого. Пусть правильным будет передать ровно один аргумент, и чтобы он был числом целым и неотрицательным. Изменения будут такие:

import Text.Read (readMaybe) -- readMaybe упоминалась выше

extractLength :: [String] -> Maybe Int
extractLength args = do
  [sLen] <- return args
  len <- readMaybe sLen
  if len > 0 then return len else Nothing


Здесь уже не нужно указывать тип для readMaybe sLen, он выведется в Int сам.

В этом коде есть на что посмотреть. Это блок do, «эксплуатирующий» уже не IO, а Maybe. Последняя сделана монадой таким образом, чтобы производить вычисления до тех пор, пока не получится Nothing. После этого они заканчиваются. Точно это записывается как

Nothing >>= f  =  Nothing
Just v  >>= f  =  Just (f v)


return, который должен помещать чистое значение в монаду, здесь можно определить осмысленно только как Just. В коде этой функции они взаимозаменяемы, но если бы там использовались только функции, не зависящие от конкретно Maybe, такой код мог бы одновременно быть применим и к другим монадам, и лучше было бы не писать Just попусту.

Вернусь к объяснению кода. Почему я в самой первой строке написал [sLen] <- return args, когда это эквивалентно let [sLen] = args? Что за неуместное заворачивание-разворачивание с return и <-? Оказывается, уместное, и коды не эквивалентны. Если просто написать где-нибудь [a] = [1, 2], произойдёт ошибка сопоставления (1:2:[] не соответствует по структуре шаблону a:[], а других альтернатив не предложено), такая же хорошая по ошибочности, как и undefined. А если сопоставление устроить внтутри do, вместо ошибки будет вызвана функция fail, к несчастью входящая в класс Monad вместе с >>= и return*. Если монада может справиться с ошибкой, она реагирует на это нормально — fail как какое-то значение отправляется вниз по >>= и всё хорошо кончается. Если монада не предназначена для такого обращения, fail как правило вызывает error, и всё кончается не так хорошо — вы не ждали, а мы пришли!

К счастью, Maybe — это такая замечательная монада, что и ошибки умеет представлять (в виде Nothing), и даже кое-что похуже, но давайте по-очереди. При ошибке сопоставления возникает fail = Nothing и идёт слева в >>=, после чего уже не важно, что было написано дальше — ничего из этого не вычислится, т. к. возвращаемое значение уже определено, весь блок do вернёт Nothing, что по смыслу нам подходит. Пустой список аргументов? Два аргумента? Три? А нужен один!

Дальше:

  len <- readMaybe sLen

мы этот аргумент читаем как Int. Если не получается, опять же Nothing, уже благодаря функции readMaybe.

Потом мы ещё и проверяем число на отрицательность:

  if len > 0 then return len else Nothing

Не очень смотрится, правда. Вот бы снова в два слова! Действие-то простое: условие выполняется — пошли дальше, условие не выполняется — ошибка. И чтобы выглядело это как-то так:

  guard $ len > 0
  return len


Соответствующая функция возвращает или ошибку, или что-то такое, что всё равно не будет использоваться — идеально подойдёт (). Её даже можно написать:

guard :: Monad** m => Bool -> m ()
guard True = return ()
guard False = fail ""


Но лучше её не писать, а импортировать из Control.Monad, и изменения изменятся вот так:

import Text.Read (readMaybe)
import Control.Monad (guard)

extractLength :: [String] -> Maybe Int
extractLength args = do
  [sLen] <- return args
  len <- readMaybe sLen
  guard $ len > 0
  return len


Завтра будем читать, генерировать и писать!

* fail :: Monad m => String -> m a. Но не всякая монада может представить ошибку каким-то из типов m a. Например, тривиальная монада data Identity a = I a, которая представляет просто «обёртку» вокруг одного чистого значения, с определениями

return = I
I x >>= f = I (f x)


не имеет никакого способа представить ошибку — у всех типов Identity a столько же значений, сколько у соответствующих a, в противовес тому, что у Maybe-типов есть Nothing, у Either’ов есть целая левая половина, а у списков есть пустой.
Вместо fail при ошибке сопоставления должна использоваться mzero не существующего сейчас класса MonadZero, обозначающего монады, которые могут представлять и ошибку вычислений, см. ещё здесь (зато сейчас есть более специфический класс MonadPlus — монады с ошибками и выбором альтернатив, но и он не используется для do).

** Замечания предыдущей сноски в силе. Функция должна выглядеть так:

guard :: MonadZero m => Bool -> m ()
guard True = return ()
guard False = mzero


В Control.Monad.guard в типе стоит ограничение MonadPlus, хотя не всякая MonadZero была бы обязательно и MonadPlus, так что это тоже не лучшее решение, тем более что это никак не меняет того, что как ошибка при «раскрытии» синтаксиса do используется всё равно fail.

Honor thy error as a hidden intention
Вебсайт Найти все сообщения
Цитировать это сообщение
04-04-2014, 23:07    
Сообщение: #10
arseniiv

± ∓
Сообщений: 227
Зарегистрирован: 05.07.12

 
Завтра растянулось на несколько дней. :D

Сейчас нам понадобится ещё одна функция, о которой мы заранее могли не знать. Она будет превращать относительный путь к файлу — относительный относительно папки, в которой лежит файл программы — в абсолютный. Конечно, I/O хаскеля справляется с относительными путями, но ведь рабочий каталог при запуске программы может быть чёрт-те каким! Впрочем, можно его менять, см. модуль (единственный) System.Directory из пакета directory.*

Зато нам понадобится пакет filepath. Это не страшно, потому что предыдущий его всё равно бы требовал (и правильно делал). В этом пакете есть три модуля: кросс-платформенный System.FilePath и два моноплатформенных, в один из которых он и выливается, System.FilePath.Windows и System.FilePath.Posix. Две ссылки приведены исключительно из-за неполадок в голове, потому что интерфейс у этих моноплатформенных модулей одинаковый по очевидным причинам.

И, так как мы начинаем общаться с путями, замечу, что System.IO говорит «type FilePath = String». И вот, наконец, функция:

import System.FilePath -- где-то там, наверху…
import System.Environment (getExecutablePath, getArgs) -- нужна ещё одна функция

fromRelativePath :: FilePath -> IO FilePath
fromRelativePath filename = do
  path <- getExecutablePath
  return $ dropFileName path </> filename


Кстати, если передать ей не относительный, а абсолютный путь, она тоже сработает и выдаст его (логично, и спасибо за такое поведение и его явное описание в документации к filepath).

Итак, тут никак не обойтись без IO, потому что путь к программе может быть разный в зависимости от мира. getExecutablePath :: IO FilePath его нам даёт. После этого мы отрезаем имя файла программы с помощью dropFileName :: FilePath -> FilePath и приделываем новое имя с помощью (</>) :: FilePath -> FilePath -> FilePath, кавайного синонима функции combine. Детали насчёт разделителей пути в конце строк лучше смотреть в оставленных справочных ссылках.

Эта функция будет использоваться и при чтении алфавита, и при записи случайной строки. Чтение:

import Control.Exception (catch, IOException)

readAlphabet :: IO [Char]
readAlphabet = do
  path <- fromRelativePath "alphabet.txt"


Загружаем путь! Для самопроверки, в коде ниже path :: FilePath.

 h <- openFile path ReadMode `catch`
    (\(e :: IOException) -> exitWith fileOpenError)


h :: Handle. Открываем файл только для чтения, а если возникает ерунда (точный список исключений есть тут), просто выскакиваем из программы. fileOpenError — это очередной наш код возврата, определим его двойкой:

fileOpenError :: ExitCode -- ээ, ещё ту функцию не дописал! Куда ставить-то?
fileOpenError = ExitFailure 2


Напомню, что catch :: Exception e => IO a -> (e -> IO a) -> IO a и что ac `catch` handler выполняет ac до первого исключения, и, если так, возвращает результатом не результат ac, а результат обработчика handler. Это прекрасно соответствует полиморфному типу exitWith :: ExitCode -> a, который приспособится к любому требуемому.

Код выше не очень правилен: если нужно обрабатывать конкретные исключения с осмысленной реакцией без закрытия при этом программы, есть смысл обрабатывать только те, которые ты можешь здесь обработать — для этого есть функции catchJust, handleJust и tryJust в Control.Exception. Может быть, мы здесь с ними как-нибудь встретимся.

Чтобы можно было указать тип параметров в лямбда-выражении, нужно включить одно расширение языка, приписав на самом верху файла {-# LANGUAGE ScopedTypeVariables #-}. Это первое здесь упоминание о включении расширений и директивах GHC вообще. Не знаю, сработает ли такое в REPL’е — если нет, надо написать :set -XScopedTypeVariables.

Так, дальше:

 hSetEncoding h utf8

Ставим UTF-8. По умолчанию может быть любая ерунда, предложенная системой. У меня, например, CP-1251. :@
Ах да, функции с префиксом h берут первым аргументов Handle, а utf8 :: TextEncoding.

 hGetContents h

Читаем остаток файла с текущего места в нём. Так как мы его только открыли, прочтётся всё. hGetContents в конце само закроет файл, а ещё имеет очень удобный здесь тип Handle -> IO String, так что на этом можно заканчивать.

Теперь функция записи:

save :: String -> IO ()
save s = do
  path <- fromRelativePath "result.txt"
  h <- openFile path WriteMode `catch`
    (\(e :: IOException) -> exitWith fileOpenError)
  hSetEncoding h utf8


Всё как в прошлый раз, только ставим режим только чтения.

 hPutStr h s `catch` -- см. System.IO, тут тоже может быть исключение
    (\(e :: IOException) -> exitWith fileWriteError)


Запишем строку. Как поговаривают, тут тоже может возникнуть досадная ерунда. Так что обработаем её.

 hClose h

И закроем файл. Ой, чуть не забыл:

fileWriteError :: ExitCode
fileWriteError = ExitFailure 3


Остаётся только элемент случайности. И тут придётся вспомнить, что функция в хаскеле не может ничего изменить, а только вернуть результат — так что, использовав генератор случайных** значений, придётся вместе со значением возвратить и обновлённый генератор, чтобы потом использовать именно его, а не старый (и получать одно и то же). И вот представьте, нужно вам три значения — так что же, писать теперь

getThree :: (RandomGen g, Random a) => g -> (g, a, a, a)
getThree gen =
  let (a, gen) = random gen in
    let (b, gen) = random gen in
      let (c, gen) = random gen in (gen, a, b, c)


? Нет уж! Для удобства можно тут использовать монаду State, о которой я напишу немного потом. А сейчас объясню этот страшный, но (страшно?) поучительный код:

RandomGen g означает, что тип g представляет собой состояние генератора ПСЧ, которое можно использовать для получения значений Int в каком-то диапазоне. В частности, RandomGen StdGen, и одно состояние генератора StdGen можно получить даром от getStdGen :: IO StdGen.

Random a означает, что тип a знает как получить из Intов какого-нибудь RandomGenа свои значения. random :: (Random a, RandomGen g) => g -> (a, g) выдаст значение в интервале [minBound; maxBound], если Bounded a (Char, Int, Bool пойдут сюда), или выдаст значение в [0; 1) для таких вещей как Float и Double. Для бесконечного Integer будут генерироваться только числа из диапазона Int. А вот откуда я всё это переписал.

В тех трёх вложенных let’ах внутренние g представляют собой уже новые версии старых g. Если заменить все g на разные, получится такое:

getThree gen =
  let (a, gen') = random gen in
    let (b, gen'') = random gen' in
      let (c, gen''') = random gen'' in
        (gen''', a, b, c)


Кстати, не забывайте, что let pat = v in e — это практически то же самое, что и (\pat -> e) v.

Для нашей задачи прямое использование методов Random не подойдёт: нет какого-то предпочтительного способа получать случайные списки какого-то типа. Мы просто напишем свои функции. Сначала получим случайный символ с равномерным распределением из алфавита:

import System.Random -- пакет random

randomElem :: RandomGen g => [a] -> g -> (a, g)
randomElem xs g = (xs !! i, g') where
  (i, g') = randomR (0, length xs - 1) g


Не сказать чтобы эффективная реализация — length и !! оба O(n) — но раз уж мы здесь используем списки, других вариантов нет. В containers есть чудесные деревянные Data.Map, Data.IntMap и Data.Set, а в array ещё более подходящий нам Data.Array (в (Int)Map ключами могут быть любые значения, а в Array — только значения из конкретного для каждого массива диапазона; и, кажется, это в случае данных реальных пакетов убыстряет реализацию). Это более-менее стандартные пакеты и есть в Haskell Platform, но пока я переключусь. randomR :: RandomGen g => (a, a) -> g -> (a, g), в отличие от random, принимает пару (lo, hi) граничных значений. Если hi < lo, результат не определён.

Тип нашей функции специально написан с g -> (a, g) в конце. Это позволяет использовать randomElem список как аргумент функции getStdRandom :: (StdGen -> (a, StdGen)) -> IO a в обмен на результат. Сравните:

ghci> import System.Random
ghci> getStdRandom random :: IO Int
1984664420
ghci> getStdRandom $ randomR ('A', 'Z')
'Y'
ghci> getStdRandom $ randomElem "12345"
'4'


(Напоминаю: если вы решили определить функцию прям тут, начните определение с let и не забудьте перед этим включить многострочность :set +m.)

А теперь я познакомлю читателей с двумя полезными функциями. Одна из них переэкспортируется Prelude из Data.List, это

replicate :: Int -> a -> [a]

Она создаёт список из нескольких одинаковых вещей: replicate 5 'a' = "aaaaa", replicate 4 10 = [10,10,10,10]. Вторая функция применима к монадам (и переэкспортируется не помню откуда):

sequence :: Monad m => [m a] -> m [a]

В частности, sequence :: [IO a] -> IO [a]. Эта функция берёт список действий, выполняет их по-очереди и показывает нам список результатов. А теперь гляньте на getStdRandom $ randomElem "12345" — это IO Char! Нам нужно собрать строку из нескольких таких символов. replicate 100 (getStdRandom $ randomElem "12345") уже имеет тип [IO Char], а sequence от этого всего — IO [Char], ну или IO String. Проверка:

ghci> sequence $ replicate 100 (getStdRandom $ randomElem "12345")
"34331325255552145115322243442433234351243212441154523221242514112552121123534334​45124345325123321152"


Проверьте, сколько раз встречается в строке каждый символ, на своём любимом языке. ;)

Теперь допишем наш горестный файл.

import System.Random -- на-вер-ху-на-вер-ху

generateWord :: Int -> [Char] -> IO String
generateWord n xs = sequence $ replicate n $ getStdRandom $ randomElem xs


На следующей странице код собран вместе. И…

* Beware! Гугл может послать вас на модуль из старой версии пакета. Лучше всего перепроверить это — наверху у страниц Hackage есть ссылка «Contents», которая пошлёт на страниу пакета, где перечислены в том числе и ссылки на разные версии.

** Или псевдослучайных, но я верю в лучшее.

Honor thy error as a hidden intention
Вебсайт Найти все сообщения
Цитировать это сообщение
Создать ответ 


Переход:


Пользователи просматривают эту тему: 1 Гость(ей)