learn haskell
25 subscribers
2 photos
2 links
изучение языка haskell
Download Telegram
Классы типов

Классы типов - это интерфейсы в мире haskell. Они определяют, какими функциями должен обладать тип, и выступают как элемент системы ограничений для определения полиморфного поведения.
Классы типов могут содержать не только описание функций, которыми должен обладать тип, но и переменные:
class MyStrangeType a where
someFunc :: a -> a
someValue :: a


Классы типов нужны, чтобы ограничить множество типов, которое может работать с полиморфными функциями.
addThenDouble :: Num a => a -> a -> a
addThenDouble x y = (x + y) * 2

Функция addThenDouble может работать со всеми типами, реализующими класс типов Num.

Основные классы типов
- Ord - для типов, которые поддерживают отношение порядка, требует реализации функций сравнения.
- Eq - требует проверки на равенство/неравенство.
- Show - требует представление значения типа строкой.
- Read - принимает строку и возвращает экземпляр типа Read.
- Bounded - требует определения верхней и нижней границы значений.
- Enum - требует реализации функций для последовательно упорядоченных типов.
- Num - класс типов для чисел, требует реализации операций над числами.
- Integral - класс типов для целых чисел.
- Floating - класс типов для чисел с плавающей точкой.

Чтобы узнать определения класса типов в ghci нужно воспользоваться командой :info
ghci> :info Num
type Num :: * -> Constraint
class Num a where
(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
negate :: a -> a
abs :: a -> a
signum :: a -> a
fromInteger :: Integer -> a
-- и многое другое
Undefined

Значение undefined в haskell представляет собой ошибочное вычисление. Если мы попытаемся его вычислить (например использовать в каком-то выражении), то получим ошибку времени выполнения.
Зачем?
Значение undefined используется как заглушка для еще не реализованного функционала, которая позволит скомпилировать и запустить приложение, чтобы проверить другие его части.
someFunction :: Int -> String
someFunction x = undefined -- пока не реализовано


Благодаря тому, что haskell реализует ленивые вычисления undefined можно использовать как значение в программе, пока кто-то не захочет его вычислить. Понятное дело, что ему не место в "продакшене", но в процессе разработки undefined позволяет лучше сосредоточится на некоторых отдельных функциях или кейсах с которыми сейчас идёт работа.
👍2
deriving

Для нового типа можно автоматически сгенерировать необходимые для классов типов функции с помощью ключевого слова deriving. Это сработает если класс имеет тривиальную реализацию.
data Person = Person { 
name :: String
, age :: Int }
deriving (Show, Eq)

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

Классы типов могут зависеть от других классов типов. Например Ord зависит от Eq, значит перед реализацией Ord должен быть реализован Eq. Если же вы напишете только реализацию Ord, то получите ошибку при компиляции - No instance for 'Eq имя_вашего_класса' ....

Отдельно стоит отметить автоматическую реализацию Ord для перечислений:
data MyEnum = A | B | C | D deriving (Eq, Ord)

Компилятор будет использовать порядок следования конструкторов данных (A | B | C | D) для реализации упорядочивания экземпляров MyEnum, то есть стандартно B будет меньше C, но больше A.
👍2
Ограничение синтаксиса записей

В haskell есть удобный способ задать имена полей типа, который автоматически создаст функции доступа к значениям - это синтаксис записей. Однако этот удобный способ обладает довольно странной особенностью.
Если вы создаёте 2 типа T1 и T2 в одном пространстве имён и у них есть одинаковые имена полей, то вы получите ошибку компиляции.
data T1 = T1 { t1 :: Int, t2 :: Int }
data T2 = T2 { t1 :: String, t3 :: Int }


Компиляция примера выше выдаст ошибку Multiple declarations of ‘t1’. Это происходит из-за того, что имена функций в пространстве имён должны иметь уникальные имена.
🤔1
Реализация классов типов

Иногда тривиальные реализации через deriving не подходят или не могут быть автоматически собраны. В этом случае писать реализацию придётся самому. А если есть собственные классы типов, то тут уже никуда не деться. Реализация классов типов выполняется с помощью ключевого слова instance и называется экземпляром. То есть в отличии от языков, реализующих ООП, экземпляром именуется не конкретный объект, а, говоря языком ООП, класс реализующий интерфейс.
data TrafficLight = Red | Yellow | Green

instance Eq TrafficLight where
Red == Red = True
Green == Green = True
Yellow == Yellow = True
_ == _ = False

instance Show TrafficLight where
show Red = "Красный свет"
show Yellow = "Жёлтый свет"
show Green = "Зелёный свет"


В примере выше TrafficLight реализует классы типов Eq и Show.
Хотя в классе Eq описаны функции == и /= следует переопределить только одну функцию в объявлении экземпляра класса. Это называется минимальным полным определением класса типов – имеется в виду минимум функций, которые надо реализовать, чтобы наш тип мог вести себя так, как предписано классом. Для класса Ord таким минимумом является метод compare, который возвращает значение типа Ordering.

Именно через использование различных экземпляров классов типов реализуются ограничения безудержного полиморфизм в haskell.
Можно записать такую функцию:
sum x y = x + y

без описания сигнатуры. Если у вас отключены варнинги как ошибки (никогда так не делайте!!!), то такое запустится и даже будет работать, пока не случится страшное, и кто-то не захочет сложить число и строку. Тогда случится ошибка компиляции в месте вызова. Однако разумнее ограничить возможные типы для функции через указание классов типов, которые должен реализовывать аргумент функции:
someFunc :: (Bounded a, Enum a) => a -> a -> a


Использование классов типов позволяет чётко выразить мысли разработчика, ограничив использование функций лишь экземплярами, которые обладают определённым поведением. В примере выше, функция someFunc работает с любыми экземплярами, которые реализуют одновременно классы типов Bounded и Enum.
👍2
Тип-сумма и тип-произведение

Тип-сумма (sum type) и тип-произведение (product type) — это фундаментальные концепции из теории типов, которые отражают структуру составных типов данных в haskell.

Типы-произведения определяются посредством комбинирования двух или более существующих типов с помощью «И»:
haskell 
data AuthorName = AuthorName String String


это аналог простых структур в императивных языках, например в C:
struct author_name {
char *first_name;
char *last_name;
};


В большинстве языков это единственный способ группировки типов в структуры.

Типы-суммы позволяют комбинировать типы с помощью «ИЛИ». Самым понятным примером тут является тип Bool который может принимать одно из двух значений - True или False (вот оно ИЛИ тут и проявляется).
data Bool = False | True


Типы-суммы позволяют создавать типы набор полей которых может быть различен, но это не мешает им оставаться одним и тем же типом. Например тип имени можно представить как "имя + фамилия" или "имя+отчество+фамилия" или "инициалы+фамилия":
type FirstName = String
type LastName = String
type MiddleName = String

data Name = Name FirstName LastName
| NameWithMiddle FirstName MiddleName LastName
| TwoInitialsWithLast Char Char LastName


Чтобы получить доступ к полю каждого возможного варианта типа нужно указать имя конструктора при определении функции:
lastName :: Name -> String
lastName (Name _ n) = n
lastName (NameWithMiddle _ _ n) = n
lastName (TwoInitialsWithLast _ _ n) = n
👍2
Полугруппы

Полугруппы - это одна из концепций языка haskell позволяющая комбинировать элементы одного типа. Можно взять две сущности одного типа и комбинировать их для получения новой сущности того же типа. Аналогией может служить операция слияния различных коллекций для объединения их в новую коллекцию того же типа.

Класс Semigroup содержит всего один метод - операцию <>:
class Semigroup a where
(<>) :: a -> a -> a

Для типа Integer операция <> идентична сложению:
instance Semigroup Integer where
(<>) x y = x + y

Для каждого типа может быть довольно большое количество возможных инстансов Semigroup, но определить можно только один. Для Integer это может быть сложение, умножение, максимум и др.

Операция <> должна удовлетворять закону ассоциативности - (x <> y) <> z = x <> (y <> z). Операция сложения удовлетворяет этому закону - 1+(2+3) = (1+2)+3, а вот вычитание или деление - нет. Ассоциативность является одним из законов классов типов (laws).
1
Monoid

Monoid - это Semigroup с нейтральным элементом. Нейтральность элемента означает, что x <> id = x и id <> x = x (id от identity).
Нейтральный элемент даёт пустое (нулевое) состояние для обработки коллекций - свёрток, композиций, и позволяет работать с пустыми списками. Semigroup же подходит для случаев, когда у типа нет пустого состояния или оно бессмысленно.
class Semigroup a => Monoid a where
mempty :: a -- нейтральный элемент
mappend :: a -> a -> a -- композиция (<>)
mconcat :: [a] -> a


Для сложения целых чисел нейтральным элементом будет 0, для умножения - 1.
instance Monoid Integer where
mappend = (+)
mempty = 0

-- или (но не одновременно)
instance Monoid Integer where
mappend = (*)
mempty = 1


Для определения экземпляра Monoid минимальным полным определением являются mempty и mappend, а mconcat определяется автоматически:
mconcat = foldr mappend mempty


У класса Monoid есть свои законы:
1. mappend mempty x = x и mappend x mempty = x - если что-то прибавить к пустому элементу, то это что-то и получим
2. mappend x (mappend y z) = mappend (mappend x y) z - ассоциативность
3. mconcat = foldr mappend mempty - определение mconcat из минимального полного определения класса.

Многие встроенные функции haskell требуют, чтобы типом аргумента был экземпляр Monoid. Monoid используется для свёрток через функции fold и foldMap (например суммирование чисел, склеивание строк), накопления результатов в рекурсивных алгоритмах, параллельных обработок (а-ля MapReduce).
totalSalary :: [Int] -> Int
totalSalary = getSum . foldMap Sum

totalSalary [3000, 4000, 5000]
totalSalary []


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

Моноиды так же могут обладать дополнительными свойствами:
- коммутативность - все элементы можно поменять местами (1+2==2+1). Не для всех это работает, для списков например нет ([1,2]++[3,4] != [3,4]++[1,2])
- идемпотентность - если x <> x = x. Списки и числа под сложение/умножение - не идемпотентные, а множество и объединения по логическому ИЛИ или И - идемпотентные.
- дуальность - наличие моноида с обратным порядком применения операций.
👍2
Hackage и Hoogle

Hackage ( https://hackage.haskell.org ) - это официальный репозиторий пакетов haskell с открытым исходным кодом. Название репозитория происходит от слияния слов Haskell и package. Он содержит тысячи библиотек и приложений, организованных в пакеты. Hackage является основным источником пакетов для менеджеров сборки cabal и stack.
Стандартная библиотека включает в себя более сотни модулей, но есть среди них самый известный, носящий имя Prelude, его содержимое автоматически импортируется во все модули наших проектов.
Каждый пакет включает:
- исходный код
- метаданные (.cabal-файл с зависимостями, версией, лицензией)
- документацию (Haddock)

Hoogle ( https://hoogle.haskell.org ) - это специализированная поисковая система для hackage. Hoogle позволяет искать функции не только по имени, но еще и по типу (например: (a -> b) -> [a] -> [b]).
Hoogle можно установить локально
stack install hoogle

или интегрировать в редактор через плагины. Eсли вы используете HLS (haskell language server), то он будет использовать hoogle установленный в системе.
👍3
Модули

При помощи модулей можно группировать программные сущности в отдельные файлы. Это декларации самого верхнего уровня которые включают в себя остальные - функции, типы данных, классы типов и экземпляры типов.
В одном файле можно определить только один модуль. Делается это так:
module ModuleName where


Имя модуля должно совпадать с именем файла (не обязательно, просто так транслятор сам найдёт нужный модуль) и начинаться с заглавной буквы. Строка определения модуля должна быть первой в файле (комментарии не в счёт).
При объявлении модуля можно инкапсулировать его содержание, сделав так, чтобы наружу торчали только нужные идентификаторы. Для этого нужно их перечислить в скобках после имени файла:
modele ModuleName (
MyType0,
MyType1 (...),
MyType2 (WithOne, WithTwo)
myFunction
) where


При этом у типов можно ограничить экспорт конструкторов. В примере выше у типа MyType0 не экспортируются конструкторы и, следовательно, мне этого модуля будет доступен только идентификатор типа. Для MyType1 экспортируются все конструкторы, а для MyType2 - только перечисленные.

Импортировать модуль можно с помощью ключевого слова import. При этом можно указать имена сущностей, которые мы хотим импортировать (как в Module2) или импортировать все доступные (как в Module1). В стандартной библиотеке принято именование модулей, которое включает относительный путь к файлу модуля от точки входа в корневой каталог модуля.
import Module1
import Module2 (
myFunction1,
myFunction2
)
import Data.List -- лежит в data/list.hs


Естесвенно, что импортировать можно только те сущности, которые экспортируются указанным модулем, иначе будет ошибка.
При конфликте имён сущностей в импортируемых модулях можно:
- скрыть идентификатор с помощью hiding
- использовать квалифицированный импорт (qualified). Тогда все сущности их модуля придётся предварять именем модуля. При это можно задать псевдоним модуля через as.
import MyModule hiding (
hidingFunction
)
import qualified OtherModule as OM
...
otherFunction data = map OM.otherFunction data
...


Так как экземпляры классов не имеют имён, то они экспортируются/импортируются всегда вместе с классами и типами для которых определёны.
👍21
картинки к посту ниже
🔥1
EMACS как IDE

Emacs - хорошая операционная система, вот только редактор в ней подкачал.

Уже пару лет я использую doom emacs для ... практически всего что связано с разработкой и, естественно, для освоения haskell. С шуткой выше давно можно не согласиться (со второй частью), если использовать evil mode для emacs, который активирует vim-комбинации клавиш для редактирования текста и манипуляции с буферами.
Использовать emacs для написания кода - это сплошное удовольствие. Это не только продвинутый редактор, но и почти всё, что потребуется для разработки в одном флаконе - терминалы, справка, git-клиент, в нем можно открыть pdf или сайт как в браузере (без js и картинок, конечно).
Благодаря встроенным функциям и lsp сразу становится доступно множество функций по запуску тестов, переименованию в проекте, просмотру определений, навигации по проекту и т.д.

Чтобы настроить emacs для haskell у вас должен быть установлен hls (haskell language server).
Далее в файле init.el нужно раскомментировать строчку
(haskell +lsp +tree-sitter)    ; a language that's lazier than I am

она позволит использовать haskell-mode для редактирования файлов .hs.

Чтобы работать с комфортом нам понадобятся еще некоторые настройки - хуки для отображения сигнатуры типов и автоиндентации, а так же автоформатирование файла при сохранении:
(add-hook 'haskell-mode-hook 'turn-on-haskell-doc-mode)
(add-hook 'haskell-mode-hook 'turn-on-haskell-indentation)
(add-hook 'haskell-mode-hook #'lsp)
(add-hook 'haskell-literate-mode-hook #'lsp)
(setq haskell-stylish-on-save t)
👍2
Maybe

Это параметризованный тип, который представляет опциональное значение, то есть значение которое может отсутствовать. Такой тип решает проблем у отсутствующего значения в результате - когда вернуть что-то нужно, а вернуть нечего, например ключ отсутствует в коллекции или мы просим первый элемент в пустом списке.
Аналогами такого подхода является std::optional<T> в С++, Option<T> в rust и error-нотация в go.

Определение Maybe выглядит следующим образом:
data Maybe a = Nothing | Just a

то есть дословно - ничего или какое значение типа a.

Модуль Data.Maybe содержит функции isJust и isNothing для проверки значений в Maybe, однако проверить наличие значения лучше через старый-добрый паттерн матчинг:
haveSome :: Maybe a -> Bool
haveSome Nothing = False
haveSome (Just _) = True


Использование isJust и isNothing считается плохой практикой, из-то того, что это ближе к императивному стилю и отделяет проверку наличия значения от его извлечения.

Использование Maybe позволяет более идиоматично работать с отсутствующим значением и предохраняет от бесконечных самописных проверок на пустое значение.
1👍1
fixity

это свойство, которое определяет ассоциативность и порядок применения в haskell. Задание fixity позволяет компилятору правильно интерпретировать выражения без избыточных скобок.
Синтаксис объявления fixity:
infixl n `op`   -- левая ассоциативность, приоритет n
infixr n `op` -- правая ассоциативность, приоритет n
infix n `op` -- неассоциативный оператор, приоритет n


Всего существует 9 уровней приоритета операций, начиная с 0. 0 - это самый нисший приоритет. Еще есть приоритет 10 - применение функции (f(x)), больше ничего в него прописать нельзя, он виртуальный.
Приоритеты стандартных операторов подобраны так, чтобы выражения читались естественно. Узнать fixity оператора можно в ghci с помощью команды :i. По умолчанию все пользовательские операторы имеют infixl 9.

Для собственных операторов можно задать иное значение fixity чтобы повысить их приоритет или изменить ассоциативность:
module Main (main) where

infixl 6 <^.^>
(<^.^>) :: Int -> Int -> Int
a <^.^> b = a * b - 1

main :: IO ()
main = do
print $ show $ 2 <^.^> 3 + 2 -- = 7
print $ show $ 2 <^.^> (3 + 2) -- = 9
print $ show $ (2 <^.^> 3) + 2 -- = 7
👍4
IO

Чистые функции на то и чистые, что не имеют побочных эффектов. Но мы пишем программы именно ради побочных эффектов. Вывод чего-то на консоль, чтение или запись в файл, базу, сеть - всё это "грязные" операции, которые нам необходимы.
Haskell позволяет выполнять подобные манипуляции, но требует, чтобы они были помечены специально. Для этого существует параметризованный тип IO. Его можно сразу увидеть в функции main:
main :: IO ()
...


В данном случае IO параметризуется пустым значением - пустым кортежем. Вообще main не является функцией - она не возвращает значения, потому о main можно говорить как о действии ввода-вывода. Такими действиями являются не только main, но и, например, консольный ввод-вывод:
putStrLn :: String -> IO () -- выводит строку в консоль
getLine :: IO String -- читает строку из консоли


Для последовательного выполнения (не вычисления) действий (не функций) используется do-нотация. Она позволяет работать с IO типами, как с обычными типами:
someFunc :: String -> String
...

main :: IO ()
main = do
line <- getLine
let someValue = someFunc line


В примере выше мы выполняем действие чтения строки из консоли в переменную и далее передаём её в функцию someFunc. Но getLine возвращает IO String, а someFunc - чистая функция, которая принимает String. Проблема решается с помощью оператора <-, который извлекает значение из контекста IO и связывает его с именем переменной. Оператор <- работает только в блоке do.
👍21
return

В императивных языках программирования return прерывает выполнение текущей функции и возвращает из неё какое-то значение.
В haskell всё иначе. return - это функция и работает она совершенно иначе:
return :: Monad m => a -> m a


В haskell return ничего не прерывает, а просто создаёт действие ввода/вывода из чистого значения. Получается контекст, который инкапсулирует некоторое значение. То есть выполняет действие обратное оператору <-. Выражение return "someString" будет иметь тип IO String.
return чаще используется в блоке do для создания действий ввода/вывода, которые ничего не делают, или для того, чтобы блок do возвращал ненужное значение (в этом случае вызов return должен быть последним в блоке).
Например:
checkNumber :: Int -> IO ()
checkNumber x = do
if x > 0
then putStrLn "Положительное"
else return ()
putStrLn "Проверка завершена"

В коде выше обе ветки выражения if должны иметь один тип.

Примечание
В современном haskell return не используют. Вместо этого используют pure.
👍1
Консольный ввод-вывод

Полезные функции и действия консольного ввода-вывода из стандартной библиотеки:

putStr и putStrLn
putStr :: String -> IO ()
putStrLn :: String -> IO ()

Принимают строку как параметр и печатают её на экране. При этом putStrLn еще выполняет перевод строки.

putChar
putChar :: Char -> IO ()

Принимает символ и печатает его на экране. Функция putStr определена рекурсивно с помощью putChar.

print
print :: Show a => a -> IO ()

Принимает значение любого экземпляра класса типов Show, вызывает функцию show, чтобы получить строковое представление и выводит его на экран через putStrLn.
Функция print вызывается каждый раз, когда нужно отобразить результат вычислений в интерпретаторе ghci.

getContents
getContents :: IO String

Считывает ввод до тех пор, пока не дойдёт до символа конца файла или прерывания работы команды в терминале. getContents ленив и читает ввод по мере необходимости.

getLine и getChar
getLine :: IO String
getChar :: IO Char

getLine читает весь пользовательский ввод до символа переносы строки (\n). getChar читает один символ с консоли.

readLn
readLn :: Read a => IO a

Читает строку из консоли и преобразовывает в нужный тип - это комбинация двух действий getLine и read. При использовании readLn нужно явно указать тип (аннотация типа), который мы хотим в результате:
num <- readLn :: IO Int 

при этом может возникать ошибка парсинга (Runtime Exception), если требуемый тип не получается из строки. Чтобы такого не случалось лучше использовать readMaybe из модуля Text.Read, который не крашит программу в случае ошибки конвертации.

interact
interact :: (String -> String) -> IO ()

Это функция для потоковой обработки текста. Она берет весь ввод из stdin, применяет к нему чистую функцию и выводит результат в stdout. Является ленивой и выводит данные по мере их поступления.
👍1
Полезные функции для работы с действиями

when из Control.Monad
when :: Applicative f => Bool -> f () -> f ()

Она принимает булево значение и действие ввода-вывода, которое выполняется только если первый параметр True. Можно использовать как замену return () в выражении if:
input <- getLine
-- это
when (input == "someStr") $ do
putStrLn input
-- эквивалентно этому
if (input == "someStr")
then putStrLn input
else return ()


sequence
sequence :: (Traversable t, Monad m) => t (m a) -> m (t a)

Принимает последовательность действий ввода-вывода и возвращает одно действие ввода-вывода, последовательно выполняющее входные действия:
rs <– sequence [getLine, getLine, getLine]

создаст действие, которое выполнит getLine 3 раза.

mapM и mapM_
mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)
mapM_ :: (Foldable t, Monad m) => (a -> m b) -> t a -> m ()

Функция mapM принимает функцию и список, применяет функцию к элементам списка, сводит элементы в одно действие ввода-вывода и выполняет их. Функция mapM_ работает так же, но отбрасывает результат действия ввода-вывода.

forever из Control.Monad
forever :: Applicative f => f a -> f b

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

forM и forM_ из Control.Monad
forM :: (Traversable t, Monad m) => t a -> (a -> m b) -> m (t b)
forM_ :: (Foldable t, Monad m) => t a -> (a -> m b) -> m ()

похожи на mapM и mapM_, но параметры поменяны местами. Первый параметр это список, второй – это функция, которую надо применить к списку и затем свести действия из списка в одно действие.
Использование forM более похоже на использование цикла for в императивных языках:
forM [1, 2, 3] (\x -> putStrLn (show x))


replicateM и replicateM_ из Control.Monad
replicateM :: Applicative f => Int -> f a -> f [a]
replicateM_ :: Applicative f => Int -> f a -> f ()

предназначены для выполнения одного и того же действия (второй параметр) определённое число раз (первый параметр). Как и в прочих функциях выше _ в конце означает, что функция вернёт пустое действие (результат нам не нужен).
В replicateM список результатов строится лениво. Однако сами действия выполняются строго по порядку.
-- напечатать 5 случайных чисел до 100
numbers <- replicateM 5 (randomRIO (1, 100))
print numbers
-- напечатать 3 раза в консоль
replicateM_ 3 (putStrLn "Привет!")
👍2
cabal

Это пакетный менеджер и система сборки для haskell. Она помогает настраивать, собирать и устанавливать программное обеспечение на haskell, а также легко распространять его среди других пользователей и разработчиков.
cabal входит в состав ghcup.

Чтобы создать простое приложение с помощью cabal нужно набрать:
cabal init myapp --non-interactive

в результате будет создан каталог myapp:
└── myapp
├── app
│ └── Main.hs
├── CHANGELOG.md
└── myapp.cabal

С Main.hs всё понятно - это файл исходного кода с функцией main. Файл myapp.cabal содержит информацию о проекте (описание, версию, лицензию, автора и т.д.) и о сборке проекта:
cabal-version:      3.16 -- текущая версия cabal
name: myapp -- имя проекта
build-type: Simple -- тип сборки
extra-doc-files: CHANGELOG.md -- файл документации

common warnings -- именованная секция общих настроек
ghc-options: -Wall -Werror -- флаги сборки для компилятора

executable myapp -- секция исполняемого файла
import: warnings -- добавление именованной секции настроек
main-is: Main.hs -- файл, содержащий функцию main
build-depends: base ^>=4.22.0.0 -- зависимости проекта через запятую
, containers >= 0.6 && < 0.8
hs-source-dirs: app -- папки где искать исходники
default-language: Haskell2010 -- используемый стандарт языка


Для сборки проекта нужно ввести команду:
cabal build

для запуска:
cabal run myapp


Так как редактировать cabal-файл нужно ручками, то возникает потребность в его автоформатировании. Для этого существует утилита cabal-gild.
cabal-gild --input myapp.cabal --output myapp.cabal

Вызов такой команды отформатирует и перезапишет файл myapp.cabal.
👍2
Data.Text

Кроме типа String в haskell для работы со строками еще существует тип Text из модуля Data. В отличии от String, который является [Char], внутри Data.Text находится массив. Еще Data.Text не использует ленивые вычисления, а если они нужны, то есть тип Data.Text.Lazy.
Использование Data.Text более предпочтительно для решения реальных задач, чем String. Text лежит в памяти более компактно и итерировать по нему гораздо быстрее, чем по списку.

Модуль Data.Text предоставляет функции конвертации в String (однако применение их вычислительно дорого):
Data.Text.pack :: String -> Data.Text.Internal.Text
Data.Text.unpack :: Data.Text.Internal.Text -> String


```
Internal в имени модуля - это распространённое соглашение в организации модулей. Это, как правило, модули, которые раскрывают внутреннюю структуру пакета и нарушают его инкапсуляцию. Делают это чтобы дать пользователям доступ к низкоуровневым функциям для максимальной производительности, не загромождать публичный интерфейс функциями, которые нужны только для связи внутренних частей библиотек.


Для того, чтобы не прописывать постоянно конвертацию строк вида "строка" в Data.Text нужно использовать расширение языка OverloadedString:
haskell
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Text as Text

someText :: Text.Text
someText = "строка"`


Почти каждая функция для работы со String имеет свой аналог для Data.Text:
- lines - разбиение строки по символу переноса на [Text]
- unlines - объединение строки символом переноса
- words - разбиение строки по любым пробельным символам
- unwords - объединяет строку пробелами
- splitOn - разделяет текст по подстроке
- intercalate - обратная splitOn

Data.Text является экземпляром Semigroup и Monoid, потому для него можно использовать <> и mconcat для объединения строк.
👍1