Перепишите задачу «подсчёт частоты слов в тексте» в четырёх парадигмах: процедурной (на C), объектно‑ориентированной (на Java), функциональной (на Haskell или Scala) и логической (на Prolog). Для каждой реализации опишите преимущества и недостатки с точки зрения читаемости, тестируемости, масштабируемости и безопасности данных
Прошу прощения — вы просили, чтобы я отвечал только на вопрос пользователя. Ниже — реализации задачи «подсчёт частоты слов в тексте» в четырёх парадигмах с краткими преимуществами и недостатками (читаемость, тестируемость, масштабируемость, безопасность данных). Математические выражения в KaTeX. 1) Процедурная (C — сортировка + подсчёт) Код (упрощённый, читает stdin): ``` #include
#include
#include
#include
char *norm_word(const char *s){ size_t n=strlen(s); char *r=malloc(n+1); for(size_t i=0;i<n;i++) r[i]=tolower((unsigned char)s[i]); r[n]=0; return r; } int cmp(const void *a,const void *b){ return strcmp(*(char**)a,*(char**)b); } int main(){ size_t cap=1024, cnt=0; char **words=malloc(cap*sizeof(char*)); int c; char buf[256]; size_t p=0; while((c=getchar())!=EOF){ if(isalpha(c)){ buf[p++]=c; if(p==255) {buf[p]=0; words[cnt++]=norm_word(buf); p=0;} } else if(p){ buf[p]=0; if(cnt==cap){cap*=2; words=realloc(words,cap*sizeof(char*));} words[cnt++]=norm_word(buf); p=0; } } if(p){ buf[p]=0; words[cnt++]=norm_word(buf); } qsort(words,cnt,sizeof(char*),cmp); for(size_t i=0;i<cnt;){ size_t j=i+1; while(j<cnt && strcmp(words[i],words[j])==0) j++; printf("%s %zu\n", words[i], j-i); free(words[i]); i=j; } free(words); return 0; } ``` Временная сложность: O(nlogn)O(n \log n)O(nlogn) (из‑за сортировки). Память: O(n)O(n)O(n). Плюсы: - читаемость: понятен процедурный поток данных; - контроль над памятью и производительностью; - простота тестирования отдельных функций (токенизация, нормализация). Минусы: - больше ручной работы (менеджмент памяти) — ошибки безопасности (утечки, переполнения); - масштабируемость: сортировка в памяти плохо масштабирует на очень большие данные; - тестируемость интеграционно сложнее (работа со stdin/stdout). 2) Объектно‑ориентированная (Java — HashMap) Код (Java 8+): ``` import java.nio.file.*; import java.util.*; import java.util.regex.*; public class WordCounter { private final Map counts = new HashMap(); private static final Pattern WORD = Pattern.compile("\\p{L}+"); public void addText(String text){ Matcher m = WORD.matcher(text.toLowerCase()); while(m.find()){ counts.merge(m.group(), 1, Integer::sum); } } public Map getCounts(){ return Collections.unmodifiableMap(counts); } public static void main(String[] args) throws Exception { String txt = new String(Files.readAllBytes(Paths.get(args[0])), "UTF-8"); WordCounter wc = new WordCounter(); wc.addText(txt); wc.getCounts().forEach((w,c)->System.out.println(w+" "+c)); } } ``` Временная сложность: средняя O(n)O(n)O(n) (хеш‑таблица). Память: O(u)O(u)O(u) (уникальные слова). Плюсы: - читаемость: код высокоуровневый, легко понимать API; - тестируемость: методы легко мокать и unit‑тестировать; - масштабируемость: можно заменить Map на ConcurrentHashMap / распределённое хранилище; - безопасность данных: управление доступом через неизменяемые представления, строгие типы. Минусы: - при экстремальных входных данных возможны деградации, синхронизация при многопоточности; - JVM‑затраты памяти, GC — накладные расходы на очень большие объёмы; - нужно следить за кодировками/нормализацией Unicode. 3) Функциональная (Haskell — Map + fold) Код (упрощённый, используя text + containers): ``` {-# LANGUAGE OverloadedStrings #-} import qualified Data.Text as T import qualified Data.Text.IO as TIO import qualified Data.Map.Strict as M import Data.Char (isLetter, toLower) import Data.Text (Text) tokenize :: Text -> [Text] tokenize = filter (not . T.null) . map (T.toLower . T.pack) . words . T.unpack . T.map (\c -> if isLetter c then c else ' ') wordCount :: Text -> M.Map Text Int wordCount = foldl (\m w -> M.insertWith (+) w 1 m) M.empty . tokenize main = do txt <- TIO.getContents mapM_ (\(w,c)->TIO.putStrLn (w " " T.pack (show c))) $ M.toList (wordCount txt) ``` Временная сложность: \(O(n \log u)\) (если используется сбалансированное дерево в Map). Память: O(u)O(u)O(u). Плюсы: - читаемость: декларативный, короткий код; - тестируемость: чистые функции легко юнит‑тестировать; легко свойство‑тестирование; - масштабируемость: легко трансформировать на потоковую/ленивую обработку; параллелизация с минимальными изменениями; - безопасность данных: отсутствие побочных эффектов уменьшает ошибки. Минусы: - работа с IO и побочными эффектами требует явной модели (monads) — небольшая ступень обучения; - производительность Map (логарифмическая) может быть хуже хеш‑таблицы для очень больших наборов; - Unicode/нормализация нужно явно учитывать. 4) Логическая (Prolog — сортировка и сжатие) Код (SWI‑Prolog, упрощённый): ``` :- use_module(library(dcg/basics)). :- use_module(library(apply)). tokenize(Text, Words) :- string_lower(Text, L), split_string(L, " \t\n", ".,!?;:()-\"'", Words0), exclude(=( ""), Words0, Words). count_words(Text, Counts) :- tokenize(Text, Words), msort(Words, Sorted), % stable sort pack_counts(Sorted, Counts). pack_counts([], []). pack_counts([W|Ws], [[W,N]|Rest]) :- span(=(W), Ws, Same, RestWords), length(Same, L), N is L+1, pack_counts(RestWords, Rest). span(Pred, List, Prefix, Suffix) :- span_(Pred, List, Prefix, Suffix). span_(_, [], [], []). span_(P, [X|Xs], [X|Ps], S) :- call(P, X), !, span_(P, Xs, Ps, S). span_(_, Xs, [], Xs). ``` Использование: ?- count_words("A a b!", C). Сложность: сортировка + проход => O(nlogn)O(n \log n)O(nlogn). Память: O(n)O(n)O(n). Плюсы: - читаемость: правило/предикатная форма хороша для описания логики; - тестируемость: легко писать факт‑тесты и запросы, интеграционное тестирование интерактивно; - для задач с логическими условиями удобно формулировать фильтры/правила. Минусы: - масштабируемость: Prolog в основном однопоточный, сложнее масштабировать на большие потоки данных; - эффективность: обычно уступает императивным/функциональным реализации по скорости и памяти; - безопасность данных: меньше средств для контроля состояния и конкурентного доступа; обработка больших бинарных/Unicode потоков требует аккуратности. Краткие сопоставления (обобщённо) - Читаемость: Java/Haskell > C ≈ Prolog (зависит от опыта). - Тестируемость: Haskell/Java > Prolog > C. - Масштабируемость: Java (с инфраструктурой) и Haskell (ленивость/параллелизм) лучше; C требует ручной оптимизации; Prolog ограничен. - Безопасность данных: Haskell (иммутабельность) и Java (типизация, библиотеки) предпочтительнее; C — самый рискованный (буферы, память). Если нужно, могу дать ещё более лаконичные или развёрнутые версии кода (например, с потоковой обработкой больших файлов, многопоточностью или учётом Unicode‑нормализации).
1) Процедурная (C — сортировка + подсчёт)
Код (упрощённый, читает stdin):
```
#include #include #include #include
char *norm_word(const char *s){
size_t n=strlen(s);
char *r=malloc(n+1);
for(size_t i=0;i<n;i++) r[i]=tolower((unsigned char)s[i]);
r[n]=0;
return r;
}
int cmp(const void *a,const void *b){ return strcmp(*(char**)a,*(char**)b); }
int main(){
size_t cap=1024, cnt=0;
char **words=malloc(cap*sizeof(char*));
int c; char buf[256]; size_t p=0;
while((c=getchar())!=EOF){
if(isalpha(c)){ buf[p++]=c; if(p==255) {buf[p]=0; words[cnt++]=norm_word(buf); p=0;} }
else if(p){ buf[p]=0; if(cnt==cap){cap*=2; words=realloc(words,cap*sizeof(char*));}
words[cnt++]=norm_word(buf); p=0; }
}
if(p){ buf[p]=0; words[cnt++]=norm_word(buf); }
qsort(words,cnt,sizeof(char*),cmp);
for(size_t i=0;i<cnt;){
size_t j=i+1;
while(j<cnt && strcmp(words[i],words[j])==0) j++;
printf("%s %zu\n", words[i], j-i);
free(words[i]);
i=j;
}
free(words);
return 0;
}
```
Временная сложность: O(nlogn)O(n \log n)O(nlogn) (из‑за сортировки). Память: O(n)O(n)O(n).
Плюсы:
- читаемость: понятен процедурный поток данных;
- контроль над памятью и производительностью;
- простота тестирования отдельных функций (токенизация, нормализация).
Минусы:
- больше ручной работы (менеджмент памяти) — ошибки безопасности (утечки, переполнения);
- масштабируемость: сортировка в памяти плохо масштабирует на очень большие данные;
- тестируемость интеграционно сложнее (работа со stdin/stdout).
2) Объектно‑ориентированная (Java — HashMap)
Код (Java 8+):
```
import java.nio.file.*;
import java.util.*;
import java.util.regex.*;
public class WordCounter {
private final Map counts = new HashMap();
private static final Pattern WORD = Pattern.compile("\\p{L}+");
public void addText(String text){
Matcher m = WORD.matcher(text.toLowerCase());
while(m.find()){
counts.merge(m.group(), 1, Integer::sum);
}
}
public Map getCounts(){ return Collections.unmodifiableMap(counts); }
public static void main(String[] args) throws Exception {
String txt = new String(Files.readAllBytes(Paths.get(args[0])), "UTF-8");
WordCounter wc = new WordCounter();
wc.addText(txt);
wc.getCounts().forEach((w,c)->System.out.println(w+" "+c));
}
}
```
Временная сложность: средняя O(n)O(n)O(n) (хеш‑таблица). Память: O(u)O(u)O(u) (уникальные слова).
Плюсы:
- читаемость: код высокоуровневый, легко понимать API;
- тестируемость: методы легко мокать и unit‑тестировать;
- масштабируемость: можно заменить Map на ConcurrentHashMap / распределённое хранилище;
- безопасность данных: управление доступом через неизменяемые представления, строгие типы.
Минусы:
- при экстремальных входных данных возможны деградации, синхронизация при многопоточности;
- JVM‑затраты памяти, GC — накладные расходы на очень большие объёмы;
- нужно следить за кодировками/нормализацией Unicode.
3) Функциональная (Haskell — Map + fold)
Код (упрощённый, используя text + containers):
```
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import qualified Data.Map.Strict as M
import Data.Char (isLetter, toLower)
import Data.Text (Text)
tokenize :: Text -> [Text]
tokenize = filter (not . T.null) . map (T.toLower . T.pack) . words . T.unpack . T.map (\c -> if isLetter c then c else ' ')
wordCount :: Text -> M.Map Text Int
wordCount = foldl (\m w -> M.insertWith (+) w 1 m) M.empty . tokenize
main = do
txt <- TIO.getContents
mapM_ (\(w,c)->TIO.putStrLn (w " " T.pack (show c))) $ M.toList (wordCount txt)
```
Временная сложность: \(O(n \log u)\) (если используется сбалансированное дерево в Map). Память: O(u)O(u)O(u).
Плюсы:
- читаемость: декларативный, короткий код;
- тестируемость: чистые функции легко юнит‑тестировать; легко свойство‑тестирование;
- масштабируемость: легко трансформировать на потоковую/ленивую обработку; параллелизация с минимальными изменениями;
- безопасность данных: отсутствие побочных эффектов уменьшает ошибки.
Минусы:
- работа с IO и побочными эффектами требует явной модели (monads) — небольшая ступень обучения;
- производительность Map (логарифмическая) может быть хуже хеш‑таблицы для очень больших наборов;
- Unicode/нормализация нужно явно учитывать.
4) Логическая (Prolog — сортировка и сжатие)
Код (SWI‑Prolog, упрощённый):
```
:- use_module(library(dcg/basics)).
:- use_module(library(apply)).
tokenize(Text, Words) :-
string_lower(Text, L),
split_string(L, " \t\n", ".,!?;:()-\"'", Words0),
exclude(=( ""), Words0, Words).
count_words(Text, Counts) :-
tokenize(Text, Words),
msort(Words, Sorted), % stable sort
pack_counts(Sorted, Counts).
pack_counts([], []).
pack_counts([W|Ws], [[W,N]|Rest]) :-
span(=(W), Ws, Same, RestWords), length(Same, L), N is L+1,
pack_counts(RestWords, Rest).
span(Pred, List, Prefix, Suffix) :- span_(Pred, List, Prefix, Suffix).
span_(_, [], [], []).
span_(P, [X|Xs], [X|Ps], S) :- call(P, X), !, span_(P, Xs, Ps, S).
span_(_, Xs, [], Xs).
```
Использование: ?- count_words("A a b!", C).
Сложность: сортировка + проход => O(nlogn)O(n \log n)O(nlogn). Память: O(n)O(n)O(n).
Плюсы:
- читаемость: правило/предикатная форма хороша для описания логики;
- тестируемость: легко писать факт‑тесты и запросы, интеграционное тестирование интерактивно;
- для задач с логическими условиями удобно формулировать фильтры/правила.
Минусы:
- масштабируемость: Prolog в основном однопоточный, сложнее масштабировать на большие потоки данных;
- эффективность: обычно уступает императивным/функциональным реализации по скорости и памяти;
- безопасность данных: меньше средств для контроля состояния и конкурентного доступа; обработка больших бинарных/Unicode потоков требует аккуратности.
Краткие сопоставления (обобщённо)
- Читаемость: Java/Haskell > C ≈ Prolog (зависит от опыта).
- Тестируемость: Haskell/Java > Prolog > C.
- Масштабируемость: Java (с инфраструктурой) и Haskell (ленивость/параллелизм) лучше; C требует ручной оптимизации; Prolog ограничен.
- Безопасность данных: Haskell (иммутабельность) и Java (типизация, библиотеки) предпочтительнее; C — самый рискованный (буферы, память).
Если нужно, могу дать ещё более лаконичные или развёрнутые версии кода (например, с потоковой обработкой больших файлов, многопоточностью или учётом Unicode‑нормализации).