На главную Назад Вперёд

Язык Haskell, более сложные определения

Прямоугольник

Тут приведено несколько решений задачи: нарисуйте прямоугольник M×N, где M и N – числа, полученные программой на вход.

Везде действие main реализовано так:

main = do
  text <- getContents
  let m:n:_ = read <$> words text :: [Int]
  process m n

Сначала решение вида “цикл в цикле”, полный аналог следующего кода

void process(long m, long n) {
  for (long i = 0; i < m; i++) {
    for (long j = 0; j < n; j++) {
      std::cout << "*";
    }
    std::cout << std::endl;
  }
}

Из Хаскелла получается плохой Си, но всё же…

process m n = loopOuter 0
  where
    loopOuter i =
      | i < m = loopInner 0 >> loopOuter (i+1)
      | True  = pure ()
    
    loopInner j =
      | j < n = putStr "*" >> loopInner (j+1)
      | True  = putStrLn ""

Теперь немного более “функциональное” решение:

-- Используются стандартные функции: 
-- replicate n x  = take n $ repeat x
-- sequence_ []     = pure () 
-- sequence_ (x:xs) = x >> sequence_ xs

process m n = 
  let
    picture = replicate m $ replicate n '*'
    actions = putStrLn <$> picture
  in sequence_ actions

Конструкция case

На самом деле, определение функции при помощи нескольких уравнений, как и многие другие части языка, тоже является сокращением. А именно, сокращением для более примитивной конструкции case. Синтаксис у этой конструкции такой:

case выражение of
    шаблон_1 -> результат_1
    шаблон_2 -> результат_2
    ...
    шаблон_n -> результат_n

Например, вот два эквивалентных определения функции, определяющий, является ли длина списка чётной (аналог even . length; точкой обозначается композиция):

evenLength1 (a:b:rest) = evenLength1 rest
evenLength1 (a:[])     = False
evenLength1 []         = True

evenLength2 list = case list of
    a:b:rest -> evenLength2 rest
    a:[]     -> False
    []       -> True

Несколько уравнений хороши в том случае, когда приходится вести индукцию по более чем одному аргументу. Конструкция case же удобнее тем, что она является выражением в отличие от определения фунции.

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

совершенноОбычнаяЕдиница = 
  let
    (a,b) = undefined
  in 1

ошибкаПриПопыткеВычисления = 
  case undefined of
    (a,b) -> 1

А именно, вычисление a и b в первом из этих определений происходит только тогда, когда требуется значение a или b. Во втором же случае вычисление a и b происходит в момент вычисления всей case-конструкции.

Тем не менее, было бы странно, если бы определения констант не сводились к case-конструкции. И действительно, шаблон case-конструкции можно пометить тильдой, отложив сравнение с шаблоном до того момента, как a и b понадобятся:

совершенноОбычнаяЕдиница = 
  case undefined of
    ~(a,b) -> 1

Немного о типе IO a

На самом деле, никакой особо сильной магии в определении типа IO a нет. Одна из его возможных реализаций (к сожалению, скомпилировать её именно в таком виде по ряду причин не получится):

-- словом type в Haskell объявляются синонимы типов 
-- (аналог using из C++ или typedef из C)
type IO a = RealWorld -> (a, RealWorld)

pure :: a -> IO a
pure x = \world -> (x,world)


(>>=) :: IO a -> (a -> IO b) -> IO b
action >>= generator = \world0 ->
    case action world0 of
        (value,world1) -> generator value world1

При этом автономная программа работает так, как будто в main в качестве величины типа RealWorld подставили состояние окружающего мира (точнее, той его части, с которой программа может непосредственно взаимодействовать) на момент запуска программы.

Упражнения

  1. Как заметили некоторые, операция getContents плохо сочетается с интерпретатором. Причина такова: стандартная реализация getContents получает очередной символ только тогда, когда он кому-то требуется, запрещая при этом кому-либо другому читать данные из входного потока. Интерпретатор же периодически взаимодействует со входным потоком, а после выполнения getContents такое взаимодействие приводит к ошибке.

Реализуйте операцию getContents', которая будет полностью получать всё содержимое стандартного входа перед дальнейшей обработкой. Вам могут пригодиться действия getChar (получает очередной символ) и isEOF (определяет, можно ли ещё раз сделать getChar). Чтобы воспользоваться isEOF, в начале программы (после строчки с module) нужно написать:

import System.IO(isEOF)
import System.IO(isEOF)


getContents' = do
    eof <- isEOF
    case eof of
        True  -> pure []
        False -> do
            x  <- getChar
            xs <- getContents'
            pure $ x:xs


-- простая проверка
main = getContents' >>= putStrLn
  1. Напишите программу, которая получает на вход два числа N и K и рисует подряд K полых квадратов размера N×N. Например для N=3 и K=4 программа должна напечатать

    *** *** *** ***
    * * * * * * * *
    *** *** *** ***
squareRow size count row
  | row == 1 || row == size = concat $ replicate count $ replicate size '*' ++ " "
  | True                    = concat $ replicate count $ "*" ++ replicate (size-2) ' ' ++ "* "


main = do
    contents <- getContents
    let numbers :: [Int]
        numbers = read <$> words contents
        size:count:_ = numbers

    sequence_ $ (putStrLn . squareRow size count) <$> [1..size]
  1. Напишите программу, которая получает на вход строчку текста, после чего сжимает все последовательности подряд идущих одинаковых букв. Например, строчка “ааааббабаав” должна превратиться в “абабав”.
process (x1:x2:xs)
  | x1 == x2 = process (x2:xs)
  | True     = x1 : process (x2:xs)
process x = x

main = getLine >>= (putStrLn . process)
  1. Напишите программу, которая получает на вход строчку текста, после чего для каждой буквы, которая в этой строчке встречается, выводит количество раз, которое эта буква встречается. Например, для строчки “ааааббабаав” программа должна напечатать

    а -> 7
    б -> 3
    в -> 1
type CountList = [(Char,Integer)]

update :: Char -> CountList -> CountList
update x ((y,n):rest)
  | x == y = (y,n+1) : rest
  | True   = (y,n)   : update x rest
update x [] = [(x,1)]


displayCount :: (Char,Integer) -> String
displayCount (x,n) = (x : " -> ") ++ show n


process :: String -> CountList
process ""     = []
process (x:xs) = update x $ process xs


main = do
    line <- getLine
    sequence_ $ (putStrLn . displayCount) <$> process line

@ 2016 arbrk1, all rights reversed