Хаки МакХакер, син на най-уважаваните програмисти в Ламбда Ленд, тамън започва да се учи да програмира. Амбициозните му родители са му дали за задача да принтира цветен текст по терминала на Линукската му машина. Хаки, незнаейки как да се справи, е решил да потърси решение в интернет форумите. След любезно запитаване в БезкрайнаРекурсия, Ти си видял въпроса му. Вече преминал обичайните чудения дали да го downvote-неш, че пита нещо очевидно, или че е поредният ученик търсещ бързо решение на домашното си, виждаш че той е положил усиля, като е прочел в WikiLambdia и се е опитал да достигне до решение, но без успех. От прочита на WikiLambdia, Хаки е дефинирал няколко цветови константи и е написал заготовки (stubs) на функциите colorize
и bleach
.
От описанието на Хаки и приложения линк към статията, ставя ясно, че оформянето на текст работи по прост начин, с инструктиране на терминала посредством запазен символ (Escape
) и последваща кодова комбинация (число). Още начинаещ в Haskell, нашият младеж не знае функцията за обърщане на число към String
(низ от символи), затова е дефинирал следните две функции:
getLastDigit :: Int -> Int
dropLastDigit :: Int -> Int
getLastDigit
връща последната цифра на подадено дадено число. Или погледнато математически, остатъкът след деление на 10. Пр:
> getLastDigit 12345
5
> getLastDigit 7
7
> getLastDigit 0
0
Hint: В Haskell няма оператор, както в C-образните езици, за операцията modulo
. За целта се ползва функцията mod
.
dropLastDigit
има точно противоположна функция на getLastDigit
- тя връща число с премахната последна цифра. Т.е. цялата част при деление на 10. Обичайният оператор за деление \
няма да свърши работа, понеже за разлика от в C, в Haskell той работи само върху рационални числа.
> dropLastDigit 12345
1234
> dropLastDigit 7
0
> dropLastDigit 0
0
След като вече имаме средставата за разделяне на число на цифрите му, следва да направим списък от тях, който в последствие ще използваме за текстовата форма. Удобното рекурсивно решение генерира списък с цифрите в обратен ред. За момента това не ни притеснява и оставаме на този вариант. Алгоритъмът е както следва:
- ако числото (
x
) е по-малко от нула, повторно извикваме алгоритъма с(-x)
- ако
x
е по-малко от 10, връщаме списък с единствен елементx
- ако ли не, връщаме последната цифра на
x
конкатанирана с резултата от рекурсивното извикване с останалите му цифри
Очакваното поведение е:
> getReverseDigits 12345
[5, 4, 3, 2, 1]
> getReverseDigits (-12345)
[5, 4, 3, 2, 1]
> getReverseDigits 7
[7]
> getReverseDigits 0
[0]
Трябва ни функция, която от обръща дадена цифра към символа й. Решението не е много елегантно, но Haskell не позволява събирането на число и буква. Затова се спираме на по-простия вариант с pattern-matching по цифрите и връщане на съответния символ. Така дефинирана функцията няма да е валидана за всеки произволен вход, което не е хубаво свойство. В литературата такива функции се наричат "непълни" (partial) и Haskell ни предупреждава с warning. Решението е да наравим функцията "пълна" (total), като добавим pattern, който приема всички останали стойности и хвърля грешка. Струва си да отбележим, че функцията продължава да не е добре дефинирана за произволен вход (т.е. нецифри), но от гледна точка на компилатора всички възможности са обработени.
Използвайте следния pattern, като крайно условие:
toChar _ = error "Not a digit"
Резултат от изпълнението:
> toChar 1
'1'
> toChar 9
'9'
> toChar 10
*** Exception: Not a digit
Вече имаме всички нужни подфункции, за да напишем
itoa :: Int -> String
Тук е времето и мястото да се справим с обратния ред на цифрите. За целта ще ползваме спомагателна функция:
itoaLoop :: String -> [Int] -> String
Първият параметър е от тип String
- в него ще запазваме текущия резултат. Такъв допълнителен параметър се нарича акумулатор, понеже в него се акумулира резултата. Алгоритъма е прост:
- ако списъка от цифри е празен - върни акумулатора
- ако ли не, извикай рекурсивно с първи аргумент първата цифра конкатанирана с акумулатора и втори аргумент останалите цифри
Функцията itoa
се дефинира тривиално посредством itoaLoop
. Този прийом е много често използван във функционалното програмиране. Не е задължително спомагателната функция да използва акумулатор - понякога целта и е само емулиране на цикъл, а понякога акумулатори се ползват и извън рекурсивни функции :).
> itoaLoop "" [3, 2, 1]
-> itoaLoop "3" [2, 1]
-> itoaLoop "23" [1]
-> itoaLoop "123" []
"123"
> itoa 12345
"12345"
> itoa 7
"7"
> itoa 0
"0"
Във форума, някой друг е вече дал решението на функциите mkStyle
и mkTextStyle
. mkStyle
приема единствен аргумент - код на стил и връща низ във формата, в който терминалът очаква стиловете. Тук операторът ++
се използва за слепване (конкатанация) на два списъка.
mkTextStyle
, пък приема цвят и връща стил. Цветовете са дефинирани като една от 8 възможности, а константата textStyle
е предефинирано отместване спрямо ASCII стандарта (текстовите цветове са в интервала 30~37).
mkStyle :: Int -> String
mkStyle style = "\x1B[" ++ itoa style ++ "m"
mkTextStyle :: Int -> String
mkTextStyle color = mkStyle (color + textStyle)
Вашата задача е да дефинирате функцията
getStyle :: String -> String
, която приема една от следните низови константи и връща съответстващия стил.
String | Color |
---|---|
"blk" | black |
"red" | red |
"grn" | green |
"ylw" | yellow |
"blu" | blue |
"mgt" | magenta |
"cyn" | cyan |
"wht" | white |
"clr" | clear* |
* clr
ще бъде използан за премахване на стиловете. Обърнете внимание на съответната константа и функцията за създаване на необособен стил.
Ако входът не е някоя от очакваните константи, фукцията трябва да го върне обграден в <>
.
Пример:
> getStyle "blk"
"\x1B[30m"
> getStyle "blu"
"\x1B[34m"
> getStyle "clr"
"\x1B[0m"
> getStyle "other"
"<other>"
Всички нужни части от пъзела са вече налице. Един от ветераните съфорумец е написал функцията за премахване на стиловете - bleach
, но не и на colorize
. Всички са на мнение, че Хаки трябва да извърви последната миля сам. Дадена е подсказка, че colorize
прилича много на bleach
, но има някои съществени разлики. colorize
приема String
като входен аргумент и заменя всички обръщения към кодови думи с тяхната стойност. Кодовите думи са обградени с <>
. Така например, ако входът е "<red>hello"
, то изходът трябва да бъде "\x1B[30mhello"
. Ако ли пък е непозната комбинация, то тя трябва да остане непроменена. Обърнете внимание, че всички ключови думи са с точно три букви и са обградени със знаци. colorize
не се нуждае от спомагателни функции, всичко нужно е вече налично.
> colorize "<red>hello<clr>"
"\x1B[31mhello\x1B[0m"
> colorize "<red>hello <blu>world<clr>"
"\x1B[31mhello \x1B[34mworld\x1B[0m"
> colorize "<haskell><hsk>"
"<haskell><hsk>"
Принтирането на escape комбинациите не е много забавно. За да видите цветовете иползвайте функцията putStrLn
, например:
putStrLn (colorize "<red>hello<clr>")
Какво се случва ако изпуснете <clr>
?
Това упражнение е с цел доразвитие. Ще добавите възможност за смяна на фона, освен на текста. Ако желаете, допълнително можете да се направят и други подобрения като промяна тежестта на шрифта, спомагателни функции за markup (<ключови-думи>) и други.
Дадени са заготовки на функциите:
mkBackgroundStyle :: Int -> String
dropMarkup :: String -> String
getMarkup :: String -> String
colorize2 :: String -> String
mkBackgroundStyle
е функция която приема код на цвят и връща стил. Реализирайте в духа на mkTextStyle
.
dropMarkup
- целта на тази функция е да пропусне (отреже) всичко до затварящия markup символ (>
). Вижте функцията removeStyle
. По аналогичен начин с минимална промяна трябва да може да реализирате dropMarkup
. Погледнете и документацията на drop
и dropWhile
. dropWhile
приема условие до кога да изпуска елементи от списъка, a drop
пропуска следващите n
елемента (в случая 1
) от подадения списък.
getMarkup
- тази функция взима съдържанието, заключено между <>
. Предвиденият начин на ползване е в colorize2
, когато знаете че следва стил (pattern matching?). Тази функция е брата на dropMarkup
, но вместо dropWhile
използвайте takeWhile
- алтернативата за звимане на елементи докато даденото условие е изпълнено.
colorize2
- използва горните спомагателни функции, за да реализира по-гъвкав начин за markup. Не забравяйте с новите възможности да добавите и фонови стилове в getStyle
. Можете да използвате прификс bgr-
за новите тагове.
Пр:
> getStyle "bgr-red"
"\x1B[41m"
putStrLn (colorize2 "<bgr-wht><blk>black <bgr-blk><wht> white<clr>")
PS: Какъв проблем има новата имплементация colorize2
?