\documentclass[a4paper, final]{article}
%\usepackage{literat} % Нормальные шрифты
\usepackage[14pt]{extsizes} % для того чтобы задать нестандартный 14-ый размер шрифта
\usepackage{tabularx}
\usepackage[T2A]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage[russian]{babel}
\usepackage{amsmath}
\usepackage[left=25mm, top=20mm, right=20mm, bottom=20mm, footskip=10mm]{geometry}
\usepackage{ragged2e} %для растягивания по ширине
\usepackage{setspace} %для межстрочного интервала
\usepackage{moreverb} %для работы с листингами
\usepackage{indentfirst} % для абзацного отступа
\usepackage{moreverb} %для печати в листинге исходного кода программ
\usepackage{pdfpages} %для вставки других pdf файлов
\usepackage{tikz}
\usepackage{graphicx}
\usepackage{afterpage}
\usepackage{longtable}
\usepackage{float}



% \usepackage[paper=A4,DIV=12]{typearea}
\usepackage{pdflscape}
% \usepackage{lscape}

\usepackage{array}
\usepackage{multirow}

\renewcommand\verbatimtabsize{4\relax}
\renewcommand\listingoffset{0.2em} %отступ от номеров строк в листинге
\renewcommand{\arraystretch}{1.4} % изменяю высоту строки в таблице
\usepackage[font=small, singlelinecheck=false, justification=centering, format=plain, labelsep=period]{caption} %для настройки заголовка таблицы
\usepackage{listings} %листинги
\usepackage{xcolor} % цвета
\usepackage{hyperref}% для гиперссылок
\usepackage{enumitem} %для перечислений

% Настраиваем листинги, чтобы они использовали счётчик figure
% \AtBeginDocument{
%   \renewcommand{\thelstlisting}{\thefigure}  % Листинги используют тот же счетчик, что и рисунки
%   \renewcommand{\lstlistingname}{Рис.}    % Меняем подпись на "Рисунок"
% }

% Автоматически увеличиваем счетчик figure перед каждым листингом
% \let\oldlstlisting\lstlisting
% \renewcommand{\lstlisting}[1][]{%
%     \refstepcounter{figure}% Увеличиваем счетчик figure
%     \oldlstlisting[#1]% Вызываем оригинальную команду lstlisting
% }

\newcommand{\specialcell}[2][l]{\begin{tabular}[#1]{@{}l@{}}#2\end{tabular}}


\setlist[enumerate,itemize]{leftmargin=1.2cm} %отступ в перечислениях

\hypersetup{colorlinks,
    allcolors=[RGB]{010 090 200}} %красивые гиперссылки (не красные)

% подгружаемые языки — подробнее в документации listings (это всё для листингов)
\lstloadlanguages{ Haskell}
% включаем кириллицу и добавляем кое−какие опции
\lstset{tabsize=2,
    breaklines,
    basicstyle=\footnotesize,
    columns=fullflexible,
    flexiblecolumns,
    numbers=left,
    numberstyle={\footnotesize},
    keywordstyle=\color{blue},
    inputencoding=cp1251,
    extendedchars=true
}
\lstdefinelanguage{MyC}{
    language=Haskell,
%    ndkeywordstyle=\color{darkgray}\bfseries,
%    identifierstyle=\color{black},
%    morecomment=[n]{/**}{*/},
%    commentstyle=\color{blue}\ttfamily,
%    stringstyle=\color{red}\ttfamily,
%    morestring=[b]",
%    showstringspaces=false,
%    morecomment=[l][\color{gray}]{//},
    keepspaces=true,
    escapechar=\%,
    texcl=true
}

\textheight=24cm % высота текста
\textwidth=16cm % ширина текста
\oddsidemargin=0pt % отступ от левого края
\topmargin=-1.5cm % отступ от верхнего края
\parindent=24pt % абзацный отступ
\parskip=5pt % интервал между абзацами
\tolerance=2000 % терпимость к "жидким" строкам
\flushbottom % выравнивание высоты страниц


% Настройка листингов
\lstset{
    language=Haskell,
    extendedchars=\true,
    inputencoding=utf8,
    keepspaces=true,
    captionpos=t,
}

\begin{document} % начало документа
    


    % НАЧАЛО ТИТУЛЬНОГО ЛИСТА
    \begin{center}
        \hfill \break
        \hfill \break
        \normalsize{МИНИСТЕРСТВО НАУКИ И ВЫСШЕГО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ\\
            федеральное государственное автономное образовательное учреждение высшего образования «Санкт-Петербургский политехнический университет Петра Великого»\\[10pt]}
        \normalsize{Институт компьютерных наук и кибербезопасности}\\[10pt] 
        \normalsize{Высшая школа технологий искусственного интеллекта}\\[10pt] 
        \normalsize{Направление: 02.03.01 <<Математика и компьютерные науки>>}\\
        
        \hfill \break
        \hfill \break
        \hfill \break
        \hfill \break
        \large{Отчет по курсовой работе}\\
        \large{по дисциплине}\\
        \large{<<Функциональное программирование>>}\\
        \large{Вариант 5}\\
        \hfill \break
        
        % \hfill \break
        % \hfill \break
    \end{center}
    
    \small{ 
        \begin{tabular}{lrrl}
            \!\!\!Студент, & \hspace{2cm} & & \\
            \!\!\!группы 5130201/20102 & \hspace{2cm} & \underline{\hspace{3cm}} &Тищенко А. А. \\\\
            \!\!\!Преподаватель,\\ \hspace{-5pt}к. т. н., доц. & \hspace{2cm} &  \underline{\hspace{3cm}} &  Моторин Д. Е. \\\\
            &&\hspace{4cm}
        \end{tabular}
        \begin{flushright}
            <<\underline{\hspace{1cm}}>>\underline{\hspace{2.5cm}} 2024г.
        \end{flushright}
    }
    
    \hfill \break
    % \hfill \break
    \begin{center} \small{Санкт-Петербург, 2024} \end{center}
    \thispagestyle{empty} % выключаем отображение номера для этой страницы
    
    % КОНЕЦ ТИТУЛЬНОГО ЛИСТА
    \newpage
    
    \tableofcontents


    % \newpage
    
    % \section*{Введение}
    
    % \addcontentsline{toc}{section}{Введение}

    \newpage
    \section {Постановка задачи}

    В рамках курсовой работы необходимо реализовать два синтаксических анализатора, решающих следующие задачи:
    
    \begin{enumerate}
        \item Разработка синтаксического анализатора для обработки строк из текстового файла. Требуется:
        \begin{itemize}
            \item Читать строки, содержащие значения и бинарные операции, из текстового файла (.txt), название которого вводит пользователь.
            \item Разбирать значения, представленные целыми числами в десятичной системе счисления.
            \item Обрабатывать бинарные операции: сложение, вычитание, умножение, деление.
            \item Вычислять результат выражения и выводить его на экран.
        \end{itemize}
        
        \item Разработка синтаксического анализатора текста и генератора продолжения текста. Задачи:
        \begin{enumerate}
            \item Считать текст из файла, название которого вводит пользователь, и выполнить его синтаксический анализ:
            \begin{itemize}
                \item Разбить текст на предложения, используя следующие правила: слова состоят только из букв; предложения состоят из слов и разделяются символами: \texttt{. ! ? ; : ( )}.
                \item Удалить из текста все символы пунктуации и цифры.
            \end{itemize}
            
            \item Построить модель N-грамм:
            \begin{itemize}
                \item Использовать биграммы и триграммы.
                \item Составить словарь, где ключами являются одно слово или пара слов, а значениями — списки всех уникальных возможных продолжений.
                \item Сохранить словарь в текстовый файл (.txt).
            \end{itemize}
            
            \item Реализовать пользовательское взаимодействие:
            \begin{itemize}
                \item При вводе одного или пары слов возвращать случайную строку длиной от 2 до 15 слов, основанную на созданном словаре.
                \item Если введенное слово отсутствует в ключах словаря, выводить сообщение об этом.
            \end{itemize}
            
            \item Организовать диалог между двумя моделями N-грамм, созданными на основе двух различных текстов:
            \begin{itemize}
                \item Пользователь задает начальное слово или пару слов и количество сообщений (глубину диалога).
                \item Ответ каждой модели основывается на последнем слове (или предпоследнем, если последнее отсутствует в словаре) из предыдущего сообщения оппонента.
            \end{itemize}
        \end{enumerate}
        
        В качестве текстов для построения моделей использовать произведения Антона Павловича Чехова.
    \end{enumerate}
    

    % \newpage
    % \section {Математическое описание}

    


    \newpage
    \section{Особенности реализации}
    Согласно заданию для каждой части работы был создан отдельный проект \texttt{stack}.

    \subsection{Часть 1: Синтаксический анализ арифметических выражений}
    
    \subsubsection{Тип Parser}
    Тип \texttt{Parser} обеспечивает разбор входной строки по заданным правилам. Он принимает на вход список токенов (например, символов) и пытается разобрать их в соответствии с описанными правилами, возвращая либо результат с оставшейся частью строки, либо \texttt{Nothing}, если разбор не удался.
    
    Код типа \texttt{Parser} представлен в листинге~\ref{lst:parser_type}.  
    \begin{itemize}
        \item Вход: список токенов.
        \item Выход: результат разбора в виде \texttt{Maybe ([tok], a)}.
    \end{itemize}
    
    Для типа \texttt{Parser} определены представители классов типов для \texttt{Functor}, \texttt{Applicative} и \texttt{Alternative}. Представитель \texttt{Functor} позволяет применять функцию к результату разбора парсера. Представители \texttt{Applicative} и \texttt{Alternative} позволяют последовательно комбинировать разные парсеры и функции и составлять сложные парсеры из простых.

\begin{lstlisting}[caption={Определение типа Parser и его представителей для классов типов Functor, Applicative и Alternative.}, label={lst:parser_type}]
newtype Parser tok a =
    Parser { runParser :: [tok] -> Maybe ([tok], a)}

instance Functor (Parser tok) where
    fmap g (Parser p) = Parser $ \xs ->
        case p xs of
            Nothing -> Nothing
            Just (cs, c) -> Just (cs, g c)

instance Applicative (Parser tok) where
    pure x = Parser $ \toks -> Just (toks, x)
    Parser u <*> Parser v = Parser $ \xs ->
        case u xs of
            Nothing -> Nothing
            Just (xs', g) ->
                case v xs' of
                    Nothing -> Nothing
                    Just (xs'', x) -> Just (xs'', g x)

instance Alternative (Parser tok) where
    empty = Parser $ \_ -> Nothing
    Parser u <|> Parser v = Parser $ \xs ->
        case u xs of
            Nothing -> v xs
            z -> z
\end{lstlisting}

    \subsubsection{Работа с арифметическими операциями}
    В листинге~\ref{lst:Operation} представлен код определения класса \texttt{Operation}, а также нескольких вспомогательных функций для работы с ним. Тип используется для хранения одной из четырёх арифметических операций: сложение, вычитание, умножение и деление. Функция \texttt{operationToString} принимает значение типа \texttt{Operation} и возвращает его строковое представление. Функция \texttt{operationToOperator} также принимает значение типа \texttt{Operation}, а возвращает функцию, соответствующую арифметической операции.

\begin{lstlisting}[caption={Определение типа Operation и функций для работы с ним.}, label={lst:Operation}]
data Operation = Add | Sub | Mul | Div deriving Show

operationToString :: Operation -> String
operationToString op = case op of
    Add -> "+"
    Sub -> "-"
    Mul -> "*"
    Div -> "/" 

operationToOperator :: Operation -> (Int -> Int -> Int)
operationToOperator op = case op of
    Add -> (+)
    Sub -> (-)
    Mul -> (*)
    Div -> div 
\end{lstlisting}
    
    \subsubsection{Базовые парсеры}
    
    В этом разделе рассматриваются основные парсеры, используемые для разбора арифметических выражений. Эти парсеры являются строительными блоками для более сложных выражений. Их код представлен в листинге~\ref{lst:base_parsers}.
    
    \begin{itemize}
        \item \texttt{satisfy} — парсит символ, удовлетворяющий предикату, и возвращает его.
        \item \texttt{char} — парсит один заданный символ и возвращает его.
        \item \texttt{digit} — парсит одну цифру и возвращает в виде символа.
        \item \texttt{skipSpaces} — парсит все пробелы пока не встретить символ, который пробелом не является.
        \item \texttt{number} — парсит целое число (последовательность цифр) и возвращает в виде~\texttt{Int}.
        \item \texttt{operation} — парсит арифметическую операцию (\texttt{+}, \texttt{-}, \texttt{*}, \texttt{/}) и возвращает как значение типа \texttt{Operation}.
    \end{itemize}
    
\begin{lstlisting}[caption={Базовые парсеры}, label={lst:base_parsers}]
satisfy :: (tok -> Bool) -> Parser tok tok
satisfy pr = Parser $ \case
    (c:cs) | pr c -> Just (cs, c)
    _             -> Nothing

char :: Char -> Parser Char Char
char c = satisfy (== c)

digit :: Parser Char Char
digit = satisfy isDigit

skipSpaces :: Parser Char String
skipSpaces = many (char ' ')

number :: Parser Char Int
number = skipSpaces *> (strToInt <$> some digit)
    where 
    strToInt = foldl (\acc x -> acc * 10 + digitToInt x) 0

operation :: Parser Char Operation
operation = skipSpaces *> (
        char '+' *> pure Add <|>
        char '-' *> pure Sub <|>
        char '*' *> pure Mul <|>
        char '/' *> pure Div
    )
\end{lstlisting}    
    
    \subsubsection{Парсер expression}
    Парсер \texttt{expression}, код которого представлен в листинге~\ref{lst:expression}, парсит выражение вида \texttt{<число> <операция> <число>}. В случае успеха возвращает кортеж вида: \texttt{(Int -- левый операнд, Operation -- операция, Int -- правый операнд)}. Является комбинацей парсеров \texttt{number} и \texttt{operation}. Не чувствителен к пробелам до выражения и внутри него, между операндами и оператором. Поглощает также пробелы после выражения с помощью парсера \texttt{skipSpaces}.
    
\begin{lstlisting}[caption={Код функции expression}, label={lst:expression}]
expression :: Parser Char (Int, Operation, Int)
expression = (,,) <$> number <*> operation <*> number <* skipSpaces
\end{lstlisting}
    
    \subsubsection{Функция processExpression}
    Код функции \texttt{processExpression} представлен в листинге~\ref{lst:processExpression}.  
    Функция принимает строку, парсит её как выражение, вычисляет результат и возвращает строку с ответом. При ошибке парсинга генерирует ошибку.
    
    Вход: \texttt{String} — строка с выражением.  
    Выход: \texttt{String} — результат вычисления в формате \texttt{a op b = result}.

    Вспомогательная функция \texttt{calculateExpression} используется для вычисления результата. На вход она получает операнды и операцию, а возвращает вычисленное значение. Её код также представлен в листинге~\ref{lst:processExpression}.
    
\begin{lstlisting}[caption={Код функции processExpression}, label={lst:processExpression}]
processExpression :: String -> String
processExpression s = case runParser expression s of
    Nothing -> error $ "Не удалось прочитать выражение: \"" ++ s ++ "\""
    Just (cs, (a, op, b)) -> case cs of
        [] -> show a ++ " " ++ operationToString op ++ " " ++
                show b ++ " = " ++ show (calculateExpression (a, op, b)) ++ "\n"
        _ -> error $ "Не удалось прочитать выражение: \"" ++ s ++ "\""


calculateExpression :: (Int, Operation, Int) -> Int
calculateExpression (a, op, b) = (operationToOperator op) a b
\end{lstlisting}

    
    \subsubsection{Функция main}
    Код функции \texttt{main} представлен в листинге~\ref{lst:main}.  
    Функция \texttt{main} считывает имя файла у пользователя, читает файл, построчно обрабатывает каждое выражение с помощью \texttt{processExpression} и выводит результат.
    
    \begin{lstlisting}[caption={Код функции main}, label={lst:main}]
    main :: IO ()
    main =
        putStrLn "Введите имя файла:" >>
        getLine >>= \fileName ->
        readFile fileName >>= \content ->
        let expressions = lines content in
        putStrLn $ concatMap processExpression expressions
    \end{lstlisting}
    

    \subsection{Часть 2: Синтаксический анализ текста и генерация фраз}

\subsubsection{Функция splitText}

На первом этапе необходимо разделить текст на предложения и слова. Предложения определяются с помощью разделителей \texttt{.!?;:()}. В словах удаляются небуквенные символы и цифры. Код функции \texttt{splitText}, ответственной за разбиение текста и очистку слов, представлен в листинге~\ref{lst:splitText}. Функция принимает на вход строку -- исходный текст, а возвращает список предложений, где каждое предложение представлено в виде списка слов. 

\begin{lstlisting}[caption={Функция splitText для разбора текста на предложения и слова}, label={lst:splitText}]
splitText :: String -> [[String]]
splitText text = filter (not . null) $ map (processSentence . words) (splitSentences text)
  where
    splitSentences :: String -> [String]
    splitSentences [] = []
    splitSentences s =
        let (sentence, rest) = break isSeparator s
            rest' = dropWhile isSeparator rest
        in if null sentence
           then splitSentences rest'
           else sentence : splitSentences rest'

    isSeparator :: Char -> Bool
    isSeparator c = c `elem` ".!?;:()"

    processSentence :: [String] -> [String]
    processSentence = filter (not . null) . map cleanWord

    cleanWord :: String -> String
    cleanWord = map toLower . filter isLetter
\end{lstlisting}

\subsubsection{Функция buildDictionary}

На основе полученных предложений строится словарь, где ключами являются либо отдельные слова, либо пары слов, а значениями — списки возможных продолжений (следующее слово или пара слов для триграмм). Для этого используются биграммы и триграммы. Код функции \texttt{buildDictionary}, формирующей словарь представлен в листинге~\ref{lst:buildDictionary}.

\begin{lstlisting}[caption={Функция buildDictionary для формирования словаря N-грамм}, label={lst:buildDictionary}]
buildDictionary :: [[String]] -> Map String [String]
buildDictionary sentences =
    let bigrams   = [ (w1, w2) | s <- sentences, (w1:w2:_) <- tails s ]
        trigrams  = [ (w1, w2, w3) | s <- sentences, (w1:w2:w3:_) <- tails s ]
        singleKeys = foldr (\(w1, w2) acc -> Map.insertWith (++) w1 [w2] acc) Map.empty bigrams
        singleKeys' = foldr (\(w1, w2, w3) acc -> Map.insertWith (++) w1 [w2 ++ " " ++ w3] acc) singleKeys trigrams
        doubleKeys = foldr (\(w1, w2, w3) acc -> Map.insertWith (++) (w1 ++ " " ++ w2) [w3] acc) Map.empty trigrams
        combined = Map.unionWith (++) singleKeys' doubleKeys
    in Map.map nub combined
\end{lstlisting}

\subsection{Функция saveDictionary}
Функция \texttt{saveDictionary}, код которой представлен в листинге~\ref{lst:saveDictionary}, сохраняет словарь с N-граммами в текстовый файл. Она принимает на вход путь до файла и сам словарь, явно ничего не возвращает, но перезаписывает содержимое файла. Для получения текстового представления списков вместо стандартной функции \texttt{show}, используется \texttt{ushow} из библиотеки \texttt{unescaping-print}~\cite{unescaping-print}. \texttt{ushow} отображает кириллицу напрямую, без экранирования, в отличии от стандартной функции \texttt{show}.

\begin{lstlisting}[caption={Функция saveDictionary для сохранения словаря N-грамм в файл.}, label={lst:saveDictionary}]
saveDictionary :: FilePath -> Map String [String] -> IO ()
saveDictionary filePath dict = withFile filePath WriteMode $ \h -> 
    mapM_ (\(k,v) -> hPutStrLn h $ ushow k ++ ": " ++ ushow v) (Map.toList dict)
\end{lstlisting}

Пример первых десяти строк содержимого итогового файла представлен ниже:

\begin{verbatim}
"а тебе": ["мы"]
"а только": ["опьянела"]
"а третировала": ["его"]
"а тут": ["еще"]
"а ты": ["хорошая","рыжик","каштанка"]
"а учитель": ["удивлялся"]
"а хавронья": ["ивановна"]
"а хозяин": ["становился"]
"а чемодан": ["покачивался"]
"а через": ["месяц"]
"а ято": ["мечтал"]
\end{verbatim}

\subsubsection{Функция generatePhrase}

Программа случайным образом формирует фразу длиной от 2 до 15 слов, используя словарь. На каждом шаге выбирается случайное продолжение, пока не будут исчерпаны возможные варианты или не достигнута заданная длина. Код функции для генерации фразы приведён в листинге~\ref{lst:generatePhrase}.

\begin{lstlisting}[caption={Функция generatePhrase для генерации фразы}, label={lst:generatePhrase}]
generatePhrase :: Map String [String] -> String -> StdGen -> [String]
generatePhrase dict start initGenState =
    let (len, initGenState') = randomR (2,15 :: Int) initGenState
    in reverse $ gp start [] len initGenState'
    where
        gp :: String -> [String] -> Int -> StdGen -> [String]
        gp key acc n genState
            | n <= 0 = acc
            | otherwise =
                case Map.lookup key dict of
                    Nothing -> acc
                    Just [] -> acc
                    Just vals ->
                        let (i, newGenState) = randomR (0, length vals - 1) genState
                            next = vals !! i
                        in gp next (next:acc) (n - length (words next)) newGenState
\end{lstlisting}

Функция \texttt{processInput} (листинг~\ref{lst:processInput}) проверяет, существует ли введённое пользователем слово(или пара слов) в словаре, и если да — генерирует фразу.

\begin{lstlisting}[caption={Функция processInput для обработки пользовательского ввода}, label={lst:processInput}]
processInput :: Map String [String] -> String -> IO ()
processInput dict input =
    if Map.member input dict then
        newStdGen >>= \gen ->
        putStrLn $ unwords $ generatePhrase dict input gen
    else
        putStrLn "Нет в словаре"
\end{lstlisting}

\subsubsection{Функция twoModelsDialog}

Реализован режим, в котором две модели N-грамм, построенные на разных текстах, обмениваются сообщениями. Начальное слово или пару слов задаёт пользователь, затем модели по очереди генерируют ответы, основываясь на словах, из которых состоит последнее сообщение их собеседника. Код, реализующий диалог, представлен в листинге~\ref{lst:twoModelsDialog}.

\begin{lstlisting}[caption={Функция twoModelsDialog для организации диалога между двумя моделями}, label={lst:twoModelsDialog}]
twoModelsDialog :: Map String [String] -> Map String [String] -> String -> Int -> IO ()
twoModelsDialog dict1 dict2 start m =
    newStdGen >>= \gen ->
    let first = generatePhrase dict1 start gen
    in putStrLn ("Модель 1: (" ++ start ++ ") " ++ unwords first) >>
        loop dict1 dict2 first m
    where
        loop :: Map String [String] -> Map String [String] -> [String] -> Int -> IO ()
        loop _ _ _ 0 = return ()
        loop d1 d2 prev i =
            putStr "Модель 2: " >>
            dialogStep d2 prev >>= \resp ->
            if null resp then return () else
            putStr "Модель 1: " >>
            dialogStep d1 resp >>= \resp2 ->
            if null resp2 then return () else
            loop d1 d2 resp2 (i-1)
\end{lstlisting}

Данный подход позволяет динамически генерировать фразы и организовывать имитацию диалога между двумя текстовыми моделями, что служит демонстрацией возможностей построенной системы N-грамм.



    \newpage
    \section {Результаты работы программы}

    


    \newpage
    \section*{Заключение}
    \addcontentsline{toc}{section}{Заключение}

    

    \newpage
    \section*{Список литературы}
    \addcontentsline{toc}{section}{Список литературы}
    
    \vspace{-1.5cm}
    \begin{thebibliography}{0}
        \bibitem{unescaping-print}
        Hackage -- unescaping-print: Tiny package providing unescaping versions of show and print, URL: \url{https://hackage.haskell.org/package/unescaping-print}, Дата обращения: 09.12.2024.
    \end{thebibliography}
\end{document}