я часто сталкиваюсь с двумя точками зрения — первая "я знаком с ФП, но оно никакой выгоды в программировании не даёт", и вторая "я знаком с ФП, но так и не научился использовать его для повышения эффективности программирования". собственно, это одно и то же, разница только в том, на кого (ФП или самого человека) возлагается вина за отсутствие результата
и поскольку этот разрыв можно преодолеть, только решая практические задачи, я предлагаю устроить мастер-класс, где опытные фп-программисты будут демонстрировать фп-подход к решению различных практических задач. для того, чтобы можно было ценить насколько вы освоили фп-подход, и оценить его преимущества по сравнению с традиционным, я предлагаю начинать с попыток "учеников" самим решить предлагаемые задачи, и затем демонстрации "учителями" их решений
итак, попробуйте решить эти задачи:
1) есть список слов. надо записать в файл "a" все слова, начинающиеся на букву 'a', в файл "b" — слова на 'b' и т.д. вплоть до z
Здравствуйте, BulatZiganshin, Вы писали:
BZ>я часто сталкиваюсь с двумя точками зрения — первая "я знаком с ФП, но оно никакой выгоды в программировании не даёт", и вторая "я знаком с ФП, но так и не научился использовать его для повышения эффективности программирования". собственно, это одно и то же, разница только в том, на кого (ФП или самого человека) возлагается вина за отсутствие результата
BZ>и поскольку этот разрыв можно преодолеть, только решая практические задачи, я предлагаю устроить мастер-класс, где опытные фп-программисты будут демонстрировать фп-подход к решению различных практических задач. для того, чтобы можно было ценить насколько вы освоили фп-подход, и оценить его преимущества по сравнению с традиционным, я предлагаю начинать с попыток "учеников" самим решить предлагаемые задачи, и затем демонстрации "учителями" их решений
Любопытно. Только у меня несколько другой вариант "ученичества" — уж не знаю, готовы ли "учителя" иметь с этим дело: язык общего назначения с некоторыми функциональными возможностями, и мне интересно, насколько функционален используемый мной подход к задаче.
BZ>итак, попробуйте решить эти задачи:
BZ>1) есть список слов. надо записать в файл "a" все слова, начинающиеся на букву 'a', в файл "b" — слова на 'b' и т.д. вплоть до z
BZ>2) имеется список имён файлов в архиве:
BZ>dir1\file1 BZ>dir1\dir2\file2 BZ>dir1\dir3\file3 BZ>dir4\file4
BZ>требуется написать функцию, которая по имени каталога возвращает список файлов и подкаталогов в нём, к примеру
BZ>f "dir1" = (["file1"], ["dir2","dir3"])
BZ>a) сложностью O(кол-во файлов)
def dir_contents(dir)
pathes.
select{|path| in_dir?(path, dir)}.
map{|path| first_path_element(relative_path(path, dir))}.
uniq.
partition{|path| is_dir?(path)}
end# вспомогательные функции могут выглядеть так (а могут, например, работать с объектами файловой системы)def in_dir?(path, dir)
path.index(dir) == 0
end
def is_dir?(path)
path =~ /\\$/
end
def relative_path(path, relative_to)
path.sub(/^#{relative_to}\\/, '')end
def first_path_element(path)
path.sub(/^(\w+?)(?:(\\)\w*)?$/, '\1\2')
end
Здравствуйте, Гест, Вы писали:
Г>Любопытно. Только у меня несколько другой вариант "ученичества" — уж не знаю, готовы ли "учителя" иметь с этим дело: язык общего назначения с некоторыми функциональными возможностями, и мне интересно, насколько функционален используемый мной подход к задаче.
да, более чем, и решения кстати у тебя совершенно fp-шные. смысл тренинга в том, чтобы принести вам пользу, и я тут вижу два "фронта" — сравнить фп и императивные решения на "рабочих" языках, и сравнить удобство фп-программирования на специализированных (хаскелл/окамл/...) и обычных языках. так что пишите так как для вас естественней. в конечном счёте, я надеюсь, вы получите навык использования фп-языков на все 100, и навык использования фп-подходов к решению задач в обычных языках тоже
BZ>>б) более эффективную
Г>
имеется в виду разумеется построение промежуточной структуры данных y, которое позволит далее вычислять dir file y за время меньше O(N)
3-я задача: есть функция сжатия compress :: String -> String. ей на вход передаётся строка (или буфер и его размер — это непринципиально), а она возвращает сжатые данные. и есть аналогичная функция decompress. при этом они однопоточные. а у нас 4-процессорная машина. соответственно, нужно написать программу, читающую входные данные мегабайтными блоками, и упаковывающую "в 4 руки", и к ней программу распаковки
Здравствуйте, BulatZiganshin, Вы писали:
BZ>>>б) более эффективную
Г>>
BZ>имеется в виду разумеется построение промежуточной структуры данных y, которое позволит далее вычислять dir file y за время меньше O(N)
А, понял. Я подумал про промежуточную структуру, но решил что это выходит за рамки задачи.
Что приходит в голову сходу — это, собственно, сразу построить дерево папок:
Тогда сложность ее будет, если не ошибаюсь, O(M), где M — кол-во папок верхнего уровня.
Типа так:
dirlist = pathes.
group_by{|path| first_path_element(path)}. #складываем по папкам
map{|dir, group| [dir, group.map{|path| relative(path, dir)}]} #откусываем имя папки, в которую положилиdef dir_contents(dir)
res = dirlist.detect{|d, path| d == dir}
res && res.last || [] #last - в смысле, из массива [папка, [содержимое папки]]end
Более разумный вариант — превратить dirlist в словарь (папка — содержимое). Тогда сложность поиска, естественно, определяется сложностью поиска по словарю в целеовм языке.
dirlist_hash = dirlist.to_hash
def dir_contents(dir)
dirlist_hash[dir]
end
наверное, можно как-то умнее.
BZ>3-я задача: есть функция сжатия compress :: String -> String. ей на вход передаётся строка (или буфер и его размер — это непринципиально), а она возвращает сжатые данные. и есть аналогичная функция decompress. при этом они однопоточные. а у нас 4-процессорная машина. соответственно, нужно написать программу, читающую входные данные мегабайтными блоками, и упаковывающую "в 4 руки", и к ней программу распаковки
Мммм...
AMOUNT = 1024 * 1024
#исхожу из того, что stream.read сдвигает указатель начала входного потокаdef compress4(stream)
stream.
#превращаем поток в массив массивов из 4х элементов по 1 Мб
unfold{|stream| stream.empty? ? nil : [(1..4).map{stream.read(AMOUNT)}, stream]}.
#сжимаем, исходя из того, что map у нас достаточно умен для распараллеливания ;)
#и сразу склеиваем 4 сжатых кусочка в 1
map{|four_chunks| four_chunks.map{|chunk| compress(chunk)}.join}.
join #...и склеиваем всеend
Тут, правда, наверное, надо оговорить, что простое "склеивание" сжатых кусочков может дать абсолютно некорректный результат, в зависимости от алгоритмов сжатия
Разжатие не пишу — кажется, будет абсолютно то же самое, только compress надо заменить на decompress
Г>Тогда сложность ее будет, если не ошибаюсь, O(M), где M — кол-во папок верхнего уровня. Г>Типа так: Г>
Г>dirlist = pathes.
Г> group_by{|path| first_path_element(path)}. #складываем по папкам
Г> map{|dir, group| [dir, group.map{|path| relative(path, dir)}]} #откусываем имя папки, в которую положили
результат будет что-то типа такого:
[("dir1", ["dir2/file2", "dir3/file3", "file1"]), ...)
Г> res && res.last || [] #last - в смысле, из массива [папка, [содержимое папки]]
а не лучше писать "res? res.last : []" ? кстати path во множ. числе тоже вроде paths :)
Г>Более разумный вариант - превратить dirlist в словарь (папка - содержимое). Тогда сложность поиска, естественно, определяется сложностью поиска по словарю в целеовм языке.
ага. но для этого надо построить другой dirlist, нежели дерево
Г>#исхожу из того, что stream.read сдвигает указатель начала входного потока
Г>def compress4(stream)
Г> stream.
Г> #превращаем поток в массив массивов из 4х элементов по 1 Мб
Г> unfold{|stream| stream.empty? ? nil : [(1..4).map{stream.read(AMOUNT)}, stream]}.
Г> #сжимаем, исходя из того, что map у нас достаточно умен для распараллеливания ;)
Г> #и сразу склеиваем 4 сжатых кусочка в 1
Г> map{|four_chunks| four_chunks.map{|chunk| compress(chunk)}.join}.
Г> join #...и склеиваем все
Г>end
Г>
вот и напишите этот map в общем, это была задача именно на высокоуровневый подход к многозадачности. ваше решение мне нравится — именно в fp-шном стиле декомпозиция задачи на отдельные элементы плюс подход в стиле "обработка потока данных"
Здравствуйте, BulatZiganshin, Вы писали:
Г>>Типа так: Г>>
Г>>dirlist = pathes.
Г>> group_by{|path| first_path_element(path)}. #складываем по папкам
Г>> map{|dir, group| [dir, group.map{|path| relative(path, dir)}]} #откусываем имя папки, в которую положили
BZ>результат будет что-то типа такого:
BZ>[("dir1", ["dir2/file2", "dir3/file3", "file1"]), ...)
Ну да.
Г>> res && res.last || [] #last - в смысле, из массива [папка, [содержимое папки]]
BZ>а не лучше писать "res? res.last : []" ? кстати path во множ. числе тоже вроде paths :)
Угу.
Г>>Более разумный вариант - превратить dirlist в словарь (папка - содержимое). Тогда сложность поиска, естественно, определяется сложностью поиска по словарю в целеовм языке.
BZ>ага. но для этого надо построить другой dirlist, нежели дерево
? почему другой? вот прямо этот [ [dir, contents], ...] отлично превращается в словарь.
Г>>#исхожу из того, что stream.read сдвигает указатель начала входного потока
Г>>def compress4(stream)
Г>> stream.
Г>> #превращаем поток в массив массивов из 4х элементов по 1 Мб
Г>> unfold{|stream| stream.empty? ? nil : [(1..4).map{stream.read(AMOUNT)}, stream]}.
Г>> #сжимаем, исходя из того, что map у нас достаточно умен для распараллеливания ;)
Г>> #и сразу склеиваем 4 сжатых кусочка в 1
Г>> map{|four_chunks| four_chunks.map{|chunk| compress(chunk)}.join}.
Г>> join #...и склеиваем все
Г>>end
Г>>
BZ>вот и напишите этот map
Ой нет. Эт я даже не представляю, с чего начинать. В смысле, если писать на Рубях, нужна видимо какая-то библиотека для параллельности, а я такими вопросами не задавался
Здравствуйте, BulatZiganshin, Вы писали:
BZ> итак, попробуйте решить эти задачи: BZ> 1) есть список слов. надо записать в файл "a" все слова, начинающиеся на букву 'a', в файл "b" — слова на 'b' и т.д. вплоть до z
import Char
import List
main = do
ls <- readFile "word.txt"let fs = words ls
|> sortBy (\(x:_) (y:_) -> toLower x `compare` toLower y)
|> groupBy (\(x:_) (y:_) -> x == y)
|> filter (\((x:_):_) -> isAlpha x)
mapM_ (\f@((c:_):_) -> writeFile [c] $ unlines f) fs
infixl 0 |>
x |> f = f x
BZ> 2) имеется список имён файлов в архиве: BZ> dir1\file1 BZ> dir1\dir2\file2 BZ> dir1\dir3\file3 BZ> dir4\file4 BZ> требуется написать функцию, которая по имени каталога возвращает список файлов и подкаталогов в нём, к примеру BZ> f "dir1" = (["file1"], ["dir2","dir3"])
Не совсем понятно условие. Должен ли просмотр идти по дереву каталогов и для запроса f "dir1" выдать (["file2"], []) или же можно ограничиться просмотром имён каталогов верхнего уровня?
Если второе, то вот:
import List
dirs = [ "dir1\\file1"
, "dir1\\dir2\\file2"
, "dir1\\dir3\\file3"
, "dir4\\file4"
]
slash2tab c = if c == '\\'then"\\\t"else [c]
dirs' = map (\d -> words $ (concat $ map slash2tab d)) dirs
f'a dir = filter (\(d:_) -> init d == dir) dirs'
|> map tail
|> (\xs -> ( concat $ filter (\(x:_) -> last x /= '\\') xs
, filter (\(x:_) -> last x == '\\') xs
|> map (init.head)))
infixl 0 |>
x |> f = f x
PS. За изменениями условия задачи прямо не поспеешь... :о(
Здравствуйте, BulatZiganshin, Вы писали:
BZ>имеется в виду разумеется построение промежуточной структуры данных y, которое позволит далее вычислять dir file y за время меньше O(N)
Так у преобразования в такую структуру сложность всё равно O(кол-во файлов)...
BZ>и поскольку этот разрыв можно преодолеть, только решая практические задачи, я предлагаю устроить мастер-класс, где опытные фп-программисты будут демонстрировать фп-подход к решению различных практических задач. для того, чтобы можно было ценить насколько вы освоили фп-подход, и оценить его преимущества по сравнению с традиционным, я предлагаю начинать с попыток "учеников" самим решить предлагаемые задачи, и затем демонстрации "учителями" их решений
"Решение практических задач" мне кажется — это составление программы, которая делает что-то полезное. Поэтому предлагаемые задачи не кажутся практическими. Скорее они всего лишь демонстрируют, что на каком-то языке ФП можно записать в 5 строчках то, что в простом Си заняло бы 10 строчек. Естественно, что это не доказывает преимущество ФП, даже если считать строчки — в простой задаче эта разница просто не имеет значения,а стоит задаче стать немного нетривиальной, как обнаружится слабость стандартной библиотеки (в терминологии Си) языков ФП по сравнению с нормальными, что плохо повлияет на количество строчек.
Кстати, в Си решение обеих задач тривиально (хотя код для поиска файлов зависит от платформы). Если функциональщикам эти задачи кажутся нетривиальными, то это тоже заставляет усомниться в полезности ФП.
мы пишем программу типа far/tc, которая позволит ходить по каталогам такого архива. для этого нужно написать функцию, возвращающую список файлов/каталогов в любом каталоге:
f "" = ([], ["dir1","dir4"])
f "dir1" = (["file1"], ["dir2","dir3"])
f "dir1\dir2" = (["file2"], [])
a) сложностью O(кол-во файлов), манипулирующую непосредственно с исходным списком
б) более эффективную, манипулирующую промежуточной структурой данных, которую мы строим из списка. т.е. в этом случае мы при входе в архив строим некое эффективное представление его каталога, которое позволяет нам в дальнейшем быстро ходить по архиву
P>Кстати, в Си решение обеих задач тривиально (хотя код для поиска файлов зависит от платформы).
Ну так можно привести тривиальное сишное решение хотя бы первой задачи?
P>а стоит задаче стать немного нетривиальной, как обнаружится слабость стандартной библиотеки (в терминологии Си) языков ФП по сравнению с нормальными, P>что плохо повлияет на количество строчек.
Про слабость стандартной библиотеки, например, хаскелла или окамла — это откуда мысль? Может быть, вы ее подтвердите?
Решение первой задачи для окамла — как-то несколько многословно. Если кто-то знает окамл — может найдется лучшее решение?
open Std
open ExtList.List
open ExtString
(* group - нигде нет *)
let rec group f l = match l with
| x::xs -> let m,n = partition (fun y -> (f x) = (f y)) xs in [x :: m] @ group f n
| [] -> []
let words =
(* isAlpha тоже нигде нет *)
let filt x = let good = function
| 'a' .. 'z' | 'A' .. 'Z' -> true
| _ -> false
in (filter (fun a -> a <> "" && good a.[0]) x) in
let read = input_list stdin
in group (fun x -> x.[0]) (filt read)
let _ = iter ( fun x -> output_file (String.sub (hd x) 0 1) (String.join "\n" (x)) ) words
dmz@x200:~/prj/ocaml/shit$ wc -l ./filez
67406 ./filez
dmz@x200:~/prj/ocaml/shit$ time cat filez | ./a.out
real 0m0.201s
user 0m0.176s
sys 0m0.028s
Мой вариант первой задачи на Окамле. Не слишком красиво, зато линейная сложность (никаких сортировок).
let (|>) x f = f x;;
let words = ["Not"; "so"; "long"; "list"; "of"; "9-10"; "not"; "so"; "weird"; "words"];;
let idx s =
if s<>""then
if s.[0]>='A' && s.[0]<='Z'then Some( (int_of_char s.[0]) - 65 ) else
if s.[0]>='a' && s.[0]<='z'then Some( (int_of_char s.[0]) - 97 ) else None
else None;;
let group lst =
let a = Array.make 26 [] in
List.iter (fun s-> match idx s with Some i -> a.(i) <- s::a.(i) | None -> ()) lst; a;;
let write =
Array.iteri (fun i lst -> if lst <> [] then
Std.output_file ~filename:(i+97 |> char_of_int |> Std.string_of_char)
~text:(String.concat "\n" (List.rev lst)));;
words |> group |> write;;
Здравствуйте, Basil B, Вы писали:
dmz>>Ну так можно привести тривиальное сишное решение хотя бы первой задачи?
отлично. а теперь условия задачи меняются — имя файла определяется первыми трёмя буквами слова. открывать файл для записи каждого слова заново — очень накладно. ваши действия?
DM>Мой вариант первой задачи на Окамле. Не слишком красиво, зато линейная сложность (никаких сортировок).
Если писать деструктивно, то можно так (чуть меньше кода):
open Std
open ExtList.List
open ExtString
open ExtHashtbl
let group l =
let hsh = Hashtbl.create 1000 in
let k s = if String.length s > 0 && ((s.[0] >= 'A' && s.[0] <= 'Z') || (s.[0] >= 'a' && s.[0] <= 'z'))
then String.sub s 0 1 else""in
let step x = let key = k x in
if not (Hashtbl.exists hsh key) then Hashtbl.add hsh key (x::[])
else let item = Hashtbl.find hsh key in Hashtbl.replace hsh key (x::item)
in Enum.iter step l ; hsh
let _ =
let hsh = group (input_lines stdin) in
Enum.iter ( fun x -> if x <> ""then output_file x (String.join "\n" (Hashtbl.find hsh x) )) (Hashtbl.keys hsh)
dmz>>Ну так можно привести тривиальное сишное решение хотя бы первой задачи?
BB>Вариант №1. Do not optimize prematurely... BB>Вариант №2.
Ща тут будет дуэль на мясорубках, так что давайте по-честному.
1) Файлы проверять на то, что бы имя было из букв (сказано же, что из букв?)
2) Читать таки давайте из консоли, а то смысл зашивать список в программе?
3) Писать весь код — что бы можно было скомпилить и запустить — инклуды, main
4) Память за собой чистить
BZ>отлично. а теперь условия задачи меняются — имя файла определяется первыми трёмя буквами слова. открывать файл для записи каждого слова заново — очень накладно. ваши действия?
Ну я и так повторно файл не открываю (хотя может надо — тогда отыграем строчек у ++ )
open Std
open ExtList.List
open ExtString
(* group - нигде нет. тормозно, но можно переписать на хэшах - будет быстро. *)let rec group f l = match l with
| x::xs -> let m,n = partition (fun y -> (f x) = (f y)) xs in [x :: m] @ group f n
| [] -> []
let dump_words =
let good c = match c with
| 'A' .. 'Z' -> c
| 'a' .. 'z' -> c
| _ -> '_'in
let sanitize x = String.map good x in
let fname x = sanitize (if String.length x >= 3 then String.sub x 0 3 else x) in
let read = input_list stdin in
let filt l = filter (fun s -> s <> "") l in
let dump = iter ( fun x -> output_file (fname (hd x)) (String.join "\n" (x)) )
in dump (group fname (filt read))
let _ = dump_words
BB>На плюсах. BB>Вариант №1. Do not optimize prematurely...
#include <vector>
#include <iostream>
using namespace std;
int main(int,char**)
{
vector<string> words;
for(int i = 0, end = words.size(); i != end; ++i)
{
ofstream(words[i].substr(0,1).c_str(), ios::app) << words[i] << ' ';
}
}
Все-таки, допишите сюда, пожалуйста, кто-нибудь, чтение вордлиста из stdin — мне просто интересно, во что
выльется повторное открытие файла. И что-то надо сделать, что бы компилятор не ругался:
dmz@x200:~/prj/ocaml/shit$ g++ ./t10cc.cc
./t10cc.cc: In function ‘int main(int, char**)’:
./t10cc.cc:12: error: invalid use of incomplete type ‘struct std::ofstream’
/usr/include/c++/4.3/iosfwd:89: error: declaration of ‘struct std::ofstream’
BB>for(int i = 0, end = words.size(); i != end; ++i) BB>{ BB> ofstream(words[i].substr(0,1).c_str(), ios::app) << words[i] << ' '; BB>} BB>[/ccode]
Что будет, если в списке файлов попадется ' ' ? . ? .. ? CON ? PRN ?
BB>[ccode]
BB>ofstream* ofs[26];
... BB> ofs[c — 'a'] = new ofstream(string(1, c).c_str());
Деструкторы не сработают — файлы не закроются? В приведенном решении на окамле
все ужимки с группировкой были для того, что бы избежать открытий и закрытий файлов —
файл пишется одной атомарной функцией. Если файлы открывать, и не заботиться о
закрытии — то писанины (на окамле) сильно убавится — например, можно будет просто писать за один
проход. Причем при выходе мусор должен подсобраться, и файлы все таки закроются. А вот в ++ при таком
выделении — закроются?
dmz>>Ща тут будет дуэль на мясорубках, так что давайте по-честному.
BB>Я пас.
Тогда фиксируем результат, что если привести с++ — ный код в рабочее состояние — то это будет уже не две строчки, а как
минимум не меньше, чем в ФП-подходе? А вот если начать налагать доп. условия, типа имя файла -> первые три буквы,
или, например, попросить писать в каждый файл только уникальные строки — ++ начинает сливать?
Re[5]: Мастер-класс по ФП
От:
Аноним
Дата:
01.01.09 16:30
Оценка:
Здравствуйте, BulatZiganshin, Вы писали:
BZ>отлично. а теперь условия задачи меняются — имя файла определяется первыми трёмя буквами слова. открывать файл для записи каждого слова заново — очень накладно. ваши действия?
Здравствуйте, Гест, Вы писали:
BZ>>вот и напишите этот map
Г>Ой нет. Эт я даже не представляю, с чего начинать. В смысле, если писать на Рубях, нужна видимо какая-то библиотека для параллельности, а я такими вопросами не задавался
Я был нетрезв.
module Enumerable
def parallel_map(&block)
results = []
threads = self.to_enum(:each_with_index).
map{|obj, i| Thread.new{results[i] = block.call(obj)} }.each{|t| t.join}
results
end
end#тестируем
r = [1,2,3,4,5].parallel_map{|i| p Thread.current; i+5}
p r
Но че-то мне это не кажется особо "функциональным"
К тому же (почему я сходу не сообразил, что требуется) в руби потоки зеленые. Так что на самом деле все эти усилия никакого выигрыша на многопроцессороной машине не дадут
Впрочем, в Ruby 1.9 они уже нативные.
ЗЫ: про ошибку в задаче с папками уже понял, как ее исправить — тоже очевидно.
BZ>Здравствуйте, Гест, Вы писали:
Г>>А, понял. Я подумал про промежуточную структуру, но решил что это выходит за рамки задачи.
BZ>ну понятно же, что меньше O(N) на самом списке не получишь
Это еще вопрос, как получить O(N) для построения словаря dir1 -> [sudir1, ...] именно из списка вида
ведь нам для каждого каталога уровня N надо перечислить все каталоги уровня N+1.
получать-то потом можно за константу, не в том вопрос. Это точно решается в таком виде именно за O(N) ?
Тупо в лоб как-то так:
open Std
open ExtList.List
open ExtString
let delim = "/"
let spath s = filter (fun x -> x <> "") (String.nsplit s delim)
let rec group f l = match l with
| x::xs -> let m,n = partition (fun y -> (f x) = (f y)) xs in [x :: m] @ group f n
| [] -> []
let gen_p p =
let path = spath p in
let ml = length path
in let rec ggen l path =
if l < ml then (String.join delim (take l path), nth path (l)) :: ggen (l+1) path
else (String.join delim (take l path), "")::[]
in ggen 1 path
let gen_all l = map ( fun x -> (fst (hd x), map snd x )) (group fst (fold_left (@) [] (map gen_p l)))
(* можно хэш построить, но лень - суть понятна *)
let _ = List.accos "home" gen_all (input_list stdin)
Здравствуйте, dmz, Вы писали:
dmz> А вот если начать налагать доп. условия, типа имя файла -> первые три буквы, dmz>или, например, попросить писать в каждый файл только уникальные строки — ++ начинает сливать?
Давайте не будем превращать тред в очередное меряние языками. Исходный замысел Булата был совсем другим, и он намного интереснее.
DM>Давайте не будем превращать тред в очередное меряние языками. Исходный замысел Булата был совсем другим, и он намного интереснее.
Он про сдвигание парадигмы. Но парадигму надо двигать ради чего-то. Лично у меня она подвинулась, когда некоторые сложные задачи просто перестали делаться, при пересмотре подхода в сторону фп, рекурсии и отсутствия мутабельных данных — решились быстро и просто. Но это в топике не покажешь — и вообще это сложно-показуемая вещь. Всегда ведь можно упереться и сделать на чем угодно.
DM>Кстати, этот вариант можно еще упростить, если вспомнить, что Hashtbl.add сам добавляет значение в список, а Hashtbl.find_all выдает весь список.
Что-то он мне вообще не нравится. Надо написать на чем-то (пусть грязном) group, куда-то его запрятать, и дальше писать чисто. Кстати, хэштейбл идет ноздря в ноздрю с решением на массивах, даже иногда быстрее (правда в пределах погрешности). Так что стараться сделать максимально грязно не стоит — достаточно написать group с изменяемым состоянием и все.
Здравствуйте, BulatZiganshin, Вы писали:
BZ>отлично. а теперь условия задачи меняются — имя файла определяется первыми трёмя буквами слова. открывать файл для записи каждого слова заново — очень накладно. ваши действия?
Спешл фор dmz компилирующийся код.
#include <string>
#include <fstream>
#include <map>
typedef std::map<std::string, std::string> StringToString;
int main(int/*argc*/, char* /*argv[]*/)
{
using namespace std;
StringToString m;
ifstream in("corncob_lowercase.txt"); // http://www.mieliestronk.com/corncob_lowercase.zip
string temp;
while(in >> temp)
m[temp.substr(0,3)].append(temp).append("\n");
for(StringToString::iterator i = m.begin(), end = m.end(); i != end; ++i)
{
ofstream out(i->first.c_str());
out << i->second;
}
return 0;
}
Здравствуйте, BulatZiganshin, Вы писали:
BZ>итак, попробуйте решить эти задачи: BZ>1) есть список слов. надо записать в файл "a" все слова, начинающиеся на букву 'a', в файл "b" — слова на 'b' и т.д. вплоть до z BZ>2) имеется список имён файлов в архиве:
BZ>dir1\file1 BZ>dir1\dir2\file2 BZ>dir1\dir3\file3 BZ>dir4\file4
BZ>требуется написать функцию, которая по имени каталога возвращает список файлов и подкаталогов в нём, к примеру
BZ>f "dir1" = (["file1"], ["dir2","dir3"])
BZ>a) сложностью O(кол-во файлов) BZ>б) более эффективную
Отличные задачки, для девятиклассников. Если хочется реально продемонстрировать мастер-класс, возьми какую-нибудь более реальную задачу, результатом которой будет законченный проект. Понятное дело не слишком крупный, но и не слишком простой. Ну там, начни с архитектуры приложения, покажи как делается функциональная декомпозиция (проектирование), сравни с тем, как это выглядело бы в ООП. Где там реальный выигрыш, в чем он проявляется. Дальше реализация, тоже с указаниями и сравнениями с ООП подходом.
А вот какую задачку взять, это вопрос...
Lisp is not dead. It’s just the URL that has changed: http://clojure.org
Нормальные задачи. Похожи на куски задач, которые регулярно возникают.
Y>Если хочется реально продемонстрировать мастер-класс, возьми какую-нибудь более реальную задачу, Y>результатом которой будет законченный проект. Понятное дело не слишком крупный, но и не слишком простой.
Ну вот не вопрос. Компилятор из луа-подобного языка в байткод для условной стековой VM. Или вот — взять файлы с SQL описанием таблиц
(с констрейнтами), распарсить, сгенерировать код для доступа к базе, а так же валидаторы форм (на основе констрейнтов).
Небольшие проекты, совсем. Первый — всего сотен пять строк. Никто не хочет порешать?
Булат дал неплохие примеры, которые можно не напрягаясь поделать, по фану. Что-то сильно более сложное — никто делать не будет.
Y>Где там реальный выигрыш, в чем он проявляется. Дальше реализация, тоже с указаниями и сравнениями с ООП подходом.
Сравнение ФП vs ОО смысла не имеет, т.к. программа в функциональном стиле может также являться в каком-то смысле ОО.
Сравнивать имеет смысл императивный vs декларативный подход.
Y>А вот какую задачку взять, это вопрос...
Можно взять генератор кодов хаффмана. Прочитать байты из STDIN, построить словарь, сдампить словарь + упакованные данные в STDOUT.
dmz>Деструкторы не сработают — файлы не закроются? В приведенном решении на окамле dmz>все ужимки с группировкой были для того, что бы избежать открытий и закрытий файлов — dmz>файл пишется одной атомарной функцией. Если файлы открывать, и не заботиться о dmz>закрытии — то писанины (на окамле) сильно убавится — например, можно будет просто писать за один dmz>проход.
Чтобы файлы закрывались (в том числе при исключениях) есть RAII (так в С++ называют scoped resource management) — причем в окамле его сделать даже легче, чем в С++, поскольку есть ФВП.
-- (* RAII facilities *)fun unwind_protect(thunk, cleanup) ->
local ok, result = pcall(thunk)
if cleanup then cleanup() end
if not ok then error(result,0) else return res end
end
fun with_open_file(path,mode) ->
fun (body) ->
local file = assert(io.open(path,mode))
return unwind_protect(fun()-> body(file) end, =>file:close())
end
end
fun with_file_list() ->
fun (body) ->
local flist = {}
return unwind_protect(
fun()-> body(flist) end,
fun()->
for _,file in pairs(flist) do file:close() end
end)
end
end
Тогда задание 1 можно записать вот так:
-- (* '$' работает как в Haskell *)
-- (* '=>' делает thunk из выражения справа: '=> expr' тоже самое, что: 'fun() -> expr end' *)
print$pcall$=>
with_open_file("words.txt","rb")$fun (infile) ->
local words = infile:read("*a")
return with_file_list()$fun (filelist) ->
for word, first in words:gmatch"((%a)%a*)"do
filelist[first] or= assert(io.open(first..".txt","wb"))
filelist[first]:write(word,'\n')
end
end
end
предложенной в топике, хэнлды не закроются. Наверное, можно было впихнуть какие-нибудь auto_ptr-ы вместо голых указателей — но это уже не ко мне. ++ забыл напрочь и как-то не тянет вспоминать. Мне вот интересно, почему в окамле нет встроенных функций group и т.п.
dmz>>def makeHuffTree(symbolTupleList):
BZ>при всём нашем уважении к питону это построитель дерева, а не генератор кодов
Уел. Если приделать перестроение дерева, что бы удобно генерить код, то в объеме кода разницы нет с тем-же окамлом.
# module Huffman : sig
val encode : (int * int) list -> string -> (bool -> unit) -> unit
val decode : (int * int) list -> (unit -> bool option) -> string
end = struct
type node =
| Leaf of int
| Node of t * t
and t = int * node
let rec build roots =
match List.sort compare roots with
| [] -> invalid_arg "Huffman"
| [h] -> h
| (p1, _ as t1)::(p2, _ as t2)::t ->
build((p1 + p2, Node(t1, t2)) :: t)
let mk_tree freqs =
build((0, Leaf(-1))::List.map (fun (p, c) -> p, Leaf c) freqs)
let rec mk_table = function
| _, Leaf c -> [c, []]
| _, Node(t1, t2) ->
let cons b (c, t) = c, b::t in
List.map (cons true) (mk_table t1) @
List.map (cons false) (mk_table t2)
let encode freqs string write_bit =
let table = mk_table(mk_tree freqs) in
let rec emit char =
let byte = Char.code char in
List.iter write_bit (List.assoc byte table) in
String.iter emit string;
List.iter write_bit (List.assoc (-1) table)
let rec decode_aux read_bit t tree buf =
match t with
| _, Leaf(-1) -> Buffer.contents buf
| _, Leaf byte ->
Buffer.add_char buf (Char.chr byte);
decode_aux read_bit tree tree buf
| _, Node(t1, t2) ->
match read_bit(), t1, t2 with
| None, _, _ -> invalid_arg "decode"
| Some true, t, _ | Some false, _, t ->
decode_aux read_bit t tree buf
let decode freqs read_bit =
let tree = mk_tree freqs in
decode_aux read_bit tree tree (Buffer.create 1024)
end;;
module Huffman :
sig
val encode : (int * int) list -> string -> (bool -> unit) -> unit
val decode : (int * int) list -> (unit -> bool option) -> string
end
Re[7]: RAII in higher-order languages (Мастер-класс по ФП)
Здравствуйте, dmz, Вы писали:
dmz> Мне вот интересно, почему в окамле нет встроенных функций group и т.п.
У него стандартная библиотека вообще очень бедная по сравнению с хаскеллом.
Но при желании можно подключить такую штуку: http://github.com/kig/preludeml/tree/master/prelude.ml
хотя мне в большинстве случаев хватает ExtLib + немного отсебятины.
Здравствуйте, dmz, Вы писали:
dmz>def makeHuffTree(symbolTupleList):..
А, ясно. При наличии реализованной бинарной кучи на С++ и др. языках получится то же самое.
Вообще, пока что мы в этом треде видим, что короче и проще те решения, где есть подходящие готовые библиотечные функции.
DM>А, ясно. При наличии реализованной бинарной кучи на С++ и др. языках получится то же самое.
Ну так оно в питоне в стандартную библиотеку входит — тащить ниоткуда не надо.
DM>Вообще, пока что мы в этом треде видим, что короче и проще те решения, где есть подходящие готовые библиотечные функции.
Ну да, в общем. ФП помогает решать проблемы, которые появляются при определенной критической массе кода, или сложности, или
необходимости модификации. Это никак в рамках топика не рассмотришь, остается только или принять на веру, или самому напороться.
Здравствуйте, BulatZiganshin, Вы писали:
BZ> есть список слов. надо записать в файл "a" все слова, начинающиеся на букву 'a', в файл "b" — слова на 'b' и т.д. вплоть до z
Решил вспоминть синтаксис haskell и, проигнорировав api для записи в файл, накалякал такое решение:
Задачка, конечно, игрушечная, но что было бы будь это "реальная" задача. Как бы я тогда её решал?
Прежде всего обобщил бы постановку задачи:
Дано: источник данных, критерий разбиения данных на группы
Надо: данные разбить на группы, обработать данные в группах.
В итоге на свет явлись бы интерфейсы и классы:
interface IGroup<T>
{
boolean add(T data); //true - data accepted, false - not accepted.void done(); //no more data, group finished.
}
//обработчик произвольных данных interface IProcessor<T>
{
void add(T data);
void done(); //no more data
}
//пишет в файлclass FileProcessor implements IProcessor<String>
{
FileProcessor(String fileName) { ... }
...
}
//пишет в консольclass ConsoleProcessor implements IProcessor<String>
{
ConsoleProcessor(String prefix) { ... }
...
}
interface IFilter<T>
{
boolean match(T t);
}
//фильтр по первой буквеclass ByFirstLetter implements IFilter<String>
{
ByFirstLetter(char letter) {...}
...
}
//фильтрует и обрабатывает данные class FilteredGroup<T> implements IGroup<T>
{
private IProcessor<T> processor;
private Filter<T> filter;
public FilteredGroup(IProcessor<T> processor, Filter<T> filter)
{
this.processor = processor;
this.filter = filter;
}
public boolean add(T t)
{
if (filter.match(t))
{
processor.add(t);
return true;
}
return false;
}
public void done()
{
processor.done();
}
}
class СompositeSeqGroup<T> implements IGroup<T>
{
private ArrayList<IGroup<T>> groups;
СompositeSeqGroup(List<IGroup<T>> groups)
{
this.groups = new ArrayList<IGroup<T>>(groups);
}
public boolean add(T t)
{
for (int i = 0; i < groups.size(); i++)
{
if (groups.get(i).add(t))
{
return true;
}
}
return false;
}
public void done()
{
for (int i = 0; i < groups.size(); i++)
{
groups.get(i).done();
}
}
}
И само решение:
public static void main(String[] args)
{
ArrayList<IGroup<String>> groups = new ArrayList<IGroup<String>>();
for (char letter = 'a'; letter <= 'z'; letter++)
{
groups.add(new FilteredGroup<String>(new FileProcessor(Character.toString(letter)), //new ConsoleProcessor(Character.toString(letter)), new ByFirstLetter(letter)));
}
IGroup<String> group = new CompositeSeqGroup<String>(groups)
//Этот код может быть вынесен в класс CompositeSeqGroup, дабы не повторяться...for (int i = 0; i < args.length; i++)
{
group.add(args[i]);
}
group.done();
}
При этом в моих руках остаётся возможность менять политики обработки (как и когда сбрасывать данные в файл или слать в сокет и т.п.), можно заменить CompositeSeqGroup на AsyncCompositeGroup так, что группы не будут ждать друг-друга...
И всё это при минимальных изменениях в коде.
Пытясь же изобразить тоже самое колдовство на haskell — начинаю теряться.
Может кто-нибудь из "гуру" набросать, как будет выглядеть "обобщённое" решение 1-ой задачки в функциональном стиле?
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>В итоге на свет явлись бы интерфейсы и классы:
NGG>При этом в моих руках остаётся возможность менять политики обработки (как и когда сбрасывать данные в файл или слать в сокет и т.п.), можно заменить CompositeSeqGroup на AsyncCompositeGroup так, что группы не будут ждать друг-друга... NGG>И всё это при минимальных изменениях в коде.
мне всё это напомнило программу сложения 2+2 на 50 строк. на яве, со всеми полагающимися bells and whistles
NGG>Пытясь же изобразить тоже самое колдовство на haskell — начинаю теряться.
NGG>Может кто-нибудь из "гуру" набросать, как будет выглядеть "обобщённое" решение 1-ой задачки в функциональном стиле?
просто реализовываешь sort_and_groupOn и дальше отдаёшь ей какие тебе нужно предикаты, например
main = do readFile "words" >>== words >>== sort_and_groupOn (take 3) >= mapM_ (\list -> writeFile (take 3 (head list)) (unwords list))
это из моей станд. библиотеки:
(>>==) = flip fmap
---------------------------------------------------------------------------------------------------
---- Операции над списками ------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------
-- |Sort and Group list by function result
sort_and_groupOn f = groupOn f . sortOn f
sort_and_groupOn' f = groupOn f . sortOn' f
-- |Sort list by function result (use Schwarznegian transform)
sortOn f = map snd . sortOn' fst . map (keyval f)
-- |Sort list by function result (don't use Schwarznegian transform!)
sortOn' f = sortBy (map2cmp f)
-- |Group list by function result
groupOn f = groupBy (map2eq f)
-- Utility functions for list operations
keyval f x = (f x, x) -- |Return pair containing computed key and original value
map2cmp f x y = (f x) `compare` (f y) -- |Converts "key_func" to "compare_func"
map2eq f x y = (f x) == (f y) -- |Converts "key_func" to "eq_func"
в первую очередь ФП — это мощные средства комбинирования функций (алгоритмов). в ООП такого нет, поэтому традиционный способ описания функторов — это создать класс с виртуальной функцией, которая будет перекрываться в различных реализациях. это неудобно и писанины много. в фп ты просто передаёшь функцию как параметр и можешь легко создавать многоуровневые решения, не боясь запутаться. таким образом, пропадает необходимость описывать монолитный класс, включающий всю необходимую тебе функциональность — порграмма становится более модульной, каждая функция решает ровно одну задачу
NGG>После чего глубоко задумался.
NGG>Задачка, конечно, игрушечная, но что было бы будь это "реальная" задача. Как бы я тогда её решал?
NGG>В итоге на свет явлись бы интерфейсы и классы:
Жжуть.
NGG>Может кто-нибудь из "гуру" набросать, как будет выглядеть "обобщённое" решение 1-ой задачки в функциональном стиле?
Я не гуру и вообще не настоящий сварщик, но. Определим функцию фильтра, которая будет фильтровать ввод,
функцию преобразования — которая будет преобразовывать содержимое файла к тому виду, который нам нужен, и функцию
группировки — соответственно, для группировки, и функцию, которая обрабатывает результаты. И как нибудь так
(на абстрактном языке, но видимо, можно и на хаскелле [а может, даже на окамле]):
input = input_list
filter = filter ( start_with "[:digit:|:letter:]" )
preprocess x = x
by_prefix = ...
output = (* открываем файл и пишем, как-то так: *) iter ( fun x -> output_file (mk_fname) (String.join "\n" (x)) )
do_job_with x = input x >>> filter >>> preprocess >>> group by_prefix >>> output
Здравствуйте, dmz, Вы писали:
dmz>группировки — соответственно, для группировки, и функцию, которая обрабатывает результаты. И как нибудь так dmz>(на абстрактном языке, но видимо, можно и на хаскелле [а может, даже на окамле]):
На Ocamle при желании можно и вариант с интерфейсами и классами повторить
Здравствуйте, BulatZiganshin, Вы писали:
BZ>мне всё это напомнило программу сложения 2+2 на 50 строк. на яве, со всеми полагающимися bells and whistles
Естественно, не зря же я сделал оговорку про игрушечность Это просто иллюстрация подхода: пушкой по воробьям, как говориться.
Но пушкой и воробья и стену кирпичную пробъёшь, а в вот рагаткой стену не пробить.
Мне хочется понять как работает функциональная пушка. Пока я вижу только как мелочёвку всякую щёлкать.
NGG>>Может кто-нибудь из "гуру" набросать, как будет выглядеть "обобщённое" решение 1-ой задачки в функциональном стиле?
BZ>просто реализовываешь sort_and_groupOn и дальше отдаёшь ей какие тебе нужно предикаты, например
BZ>main = do readFile "words" >>== words >>== sort_and_groupOn (take 3) >>= mapM_ (\list -> writeFile (take 3 (head list)) (unwords list))
Типа берётся 3-й символ, а не 1-ый и предполагается, что сортировка стабильная (порядок эквивалентых слов сохраняется)?
Сортировка это дополнительный logN, зачем?
Провероки на диапазон 'a'-'z' уже не нужны?
А как изменится решение, если вместо (take 3) использовать (contains 'a'), (contains 'b')... и т.д.? Т.е. одно и тоже слово может попадать в разные файлы? И как тогда определить имя файла?
Собственно эти вопросы и тревожат.
С одной стороны, когда решение это две строчки, его можно и заново написать, если требования слегка изменятся.
С другой сторы этот подход не маштабируется "на многострочное решение" или особо хитрые ребусы.
Отсюда и мои метания
BZ>в первую очередь ФП — это мощные средства комбинирования функций (алгоритмов). в ООП такого нет, поэтому традиционный способ описания функторов — это создать класс с виртуальной функцией, которая будет перекрываться в различных реализациях. это неудобно и писанины много. в фп ты просто передаёшь функцию как параметр и можешь легко создавать многоуровневые решения, не боясь запутаться.
Угу, в плане "покомбинировать" ФП молодец. Особенно пока можно обойсь "функтором", а не интерфейсом содержащим несколько методов и пока не появились интерфейсы содержащие одинаковые методы, но с разными контрактами...
Про "писанины много" тоже согласен. Раза в два, максимум в три больше. Правда, когда я на с# заворачивал функции высших порядков большой вложенности были проблемы с тем, какие же им имена дать. Очень уже не понятно как называть функцию, которая возвращает функцию, которая возращает функцию делающую то-то и то-то
"не боясь запутаться" — это, как мне кажется, относится только к "чистым" функциям без сайд эффектов и всяких lazy-хитростей. Иначе всё равно буду бояться
BZ>таким образом, пропадает необходимость описывать монолитный класс, включающий всю необходимую тебе функциональность — порграмма становится более модульной, каждая функция решает ровно одну задачу
Допустим, что при использовании функций как первокласных сущностей необходимость описывать монолитный класс пропадает, а разве откуда-то вообще следует необходимость описывать монолитный класс (н-р, при той же оо-декомпозиции)?
Я пока вижу как фп может облегчить мне (местами) жизнь при реализации методов классов.
А как я справлялся бы в функциональном мире с мегабайтами исходных кодов, над которыми работает хотя бы десяток человек, — не представляю, к сожалению...
NGG>Угу, в плане "покомбинировать" ФП молодец. Особенно пока можно обойсь "функтором", а не интерфейсом содержащим несколько методов и пока не появились интерфейсы содержащие одинаковые методы, но с разными контрактами...
import Data.List
lst = [
"dir1\\file1", "dir1\\dir2\\file2", "dir1\\dir3\\file3", "dir4\\file4"]
f lst d = (files, nub [takeWhile (/='\\') x | x <- dirs])
where
ff [] = lst
ff xs = [drop ld x | x <- lst, take ld x == dd]
dd = d ++ "\\"
ld = length dd
(files, dirs) = partition (notElem '\\') $ ff d
Только тут сложность получается не O(N), где N — количество путей в списке, а как минимум O(N*M*К) где M — длинна запрашиваемого пути и К — общее количество файлов начиная с запрашиваемого корня.
Здравствуйте, FR, Вы писали:
NGG>>Угу, в плане "покомбинировать" ФП молодец. Особенно пока можно обойсь "функтором", а не интерфейсом содержащим несколько методов и пока не появились интерфейсы содержащие одинаковые методы, но с разными контрактами...
FR>Так для нескольких функций есть модули, которые в функциональных языках нечто среднее между модулями и объектами из императивных, например в OCaml http://people.cis.ksu.edu/~ab/Courses/505/fall08/htmlman-3.07/manual004.html Кстати функтор в терминологии Ocaml'а http://people.cis.ksu.edu/~ab/Courses/505/fall08/htmlman-3.07/manual004.html#htoc15 манипулирует как раз интерфейсами содержащими несколько методов.
Т.е. это что-то из той же области что и классы в haskell? Механизмы позволяющие опеределить интерфейс для которого можно задать разные реализации.
У меня почему-то стойкое подозрение, что если пустить их в ход, то лаконичности в функциональном решении станет ощутимо меньше, зато гибкости прибудет.
Здравствуйте, Tonal-, Вы писали:
T>Задача (а) как-то так:
как-то всё это сложно
f list dir = list
.$ mapMaybe (stripPrefix (make dir)) -- отберём элементы внутри каталога dir
-- и отрежем от них префикс dir\
.$ partition ('\\' `notElem`) -- разделим на файлы и каталоги
.$ mapSnd (nub . map (takeWhile (/='\\'))) -- удалим дубликаты из списка каталогов
-- Добавить \ к имени каталога, если оно непустое
make "" = ""
make dir = dir++"\\"-- Функция, которая у других авторов обозначается как |>
-- В общем-то, заменяет нам вызов метода класса :)
a.$f = f a
-- Ещё одна функция из моей станд. библиотеки
mapSnd f (a,b) = (a, f b)
T>Только тут сложность получается не O(N), где N — количество путей в списке, а как минимум O(N*M*К) где M — длинна запрашиваемого пути и К — общее количество файлов начиная с запрашиваемого корня.
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>Т.е. это что-то из той же области что и классы в haskell? Механизмы позволяющие опеределить интерфейс для которого можно задать разные реализации.
Ну по сути да, но кажется поближе к объектам чем в Haskell
NGG>У меня почему-то стойкое подозрение, что если пустить их в ход, то лаконичности в функциональном решении станет ощутимо меньше, зато гибкости прибудет.
Угу, вот только у меня такое же стойкое подозрение, что для этой задачи (смотрю на последнее решение Булата) и для многих других никакие интерфейсы просто не нужны, и результат при этом не менее гибкий.
Здравствуйте, FR, Вы писали:
FR>Угу, вот только у меня такое же стойкое подозрение, что для этой задачи (смотрю на последнее решение Булата) и для многих других никакие интерфейсы просто не нужны, и результат при этом не менее гибкий.
Не соглашусь с последней частью. Я привёл уже вопросы по решению Булата, как раз касательно гибкости... пока ответов нет.
По поводу первой части: я целиком и полностью согласен, что для _этой_ задачи можно обойтись без чего угодно.
Но если решается "реальная" задача (а не игрушечная), имеет смысл обобщить слегка задачу, чтобы прекрыть себе тылы на случай изменений в требованиях. Поэтому я привёл обобщённый вариант нашей игрушечной задачи и его решение в оо-стиле. Это тоже игра Смысл игры в том, чтобы показать как "тупое" ооп может привести к многословному, но достаточно гибкому решению.
Я просил показать, как такой же гибкости достичь методами родными для фя и как-нибудь их озвучить по-возможности. Мне эта тема очень интересна и я не первый раз задаю на rsdn.ru этот глупый вопрос
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>Здравствуйте, FR, Вы писали:
FR>>Угу, вот только у меня такое же стойкое подозрение, что для этой задачи (смотрю на последнее решение Булата) и для многих других никакие интерфейсы просто не нужны, и результат при этом не менее гибкий.
NGG>Не соглашусь с последней частью. Я привёл уже вопросы по решению Булата, как раз касательно гибкости... пока ответов нет.
Я тоже не гуру, но решение приведу
Повторил точь-в-точь ваше решение.
filterGroup :: [a] -> [(a -> Bool, a -> b)] -> [([a], a -> b)]
filterGroup _ [] = []
filterGroup d (x:xs) = (fst part, snd x) : (filterGroup (snd part) xs) where
part = partition (fst x) d
stdOutput :: (Show a) => String -> a -> IO ()
stdOutput s x = do
putStr $ s ++ ": "
print x
processGroups :: [a] -> [(a -> Bool, a -> b)] -> [[b]]
processGroups d f = map (\x -> map (snd x) (fst x)) $ filterGroup d f
processGroupsIO d f = mapM_ sequence_ $ processGroups d f
filters :: [(String -> Bool, String -> IO ())]
filters = [(('a' `elem`), stdOutput "a"), (('a' `notElem`), stdOutput "not a"), (null, stdOutput "empty")]
theData = ["ala", "dfkja", "jrgh", "", "sejhr", "drt"]
main = let
byFirstLetter c [] = False
byFirstLetter c (x:xs) = x == c
makeFilter :: Char -> (String -> Bool, String -> IO ())
makeFilter c = (byFirstLetter c, stdOutput [c])
groups = map makeFilter ['a'..'z']
in
processGroupsIO theData groups
В ФП ещё и проще получается. Нету лишнего IFilter, так как IGroup всё равно принимает a, а возаращает Bool, т.е. сама является фильтром. Почему у вас FilteredGroup принимает IProcessor тоже неясно. Группировать фильтры в ФП вообще плёвое дело, просто композиция функций.
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>По поводу первой части: я целиком и полностью согласен, что для _этой_ задачи можно обойтись без чего угодно. NGG>Но если решается "реальная" задача (а не игрушечная), имеет смысл обобщить слегка задачу, чтобы прекрыть себе тылы на случай изменений в требованиях. Поэтому я привёл обобщённый вариант нашей игрушечной задачи и его решение в оо-стиле. Это тоже игра Смысл игры в том, чтобы показать как "тупое" ооп может привести к многословному, но достаточно гибкому решению. NGG>Я просил показать, как такой же гибкости достичь методами родными для фя и как-нибудь их озвучить по-возможности. Мне эта тема очень интересна и я не первый раз задаю на rsdn.ru этот глупый вопрос
Кстати, не соглашусь с реальностью задачи. Дело не в "реальности", а в сложности и потенциальном усложнении при небольшом изменении. Пиши я в ООП, я бы тоже подумал о том, как можно комбинировать фильтры, группы, написал бы кучу интерфейсов, потому что понимал бы, что если внести одно дополнительное требование, то из 50 строк придётся выкинуть 20 и ещё 50 написать, и не дай бог эти интерфейсы используются где-то ещё, придётся и там код менять. Лучше подумать об этом заранее. Тут же было 2(3?) строки, стало 7 (при этом 2 — указание типов функций, 1 — перенос строки). Их и с нуля можно написать, так что заранее напрягаться не стоит.
Вся обобщённая задача сводится к "сгруппировать, выполнить над группами действие". Т.е. тип всей задачи: [a] -> [[a]] -> [[b]].
Первая часть достигается group, если не хватает — groupBy, если не хватает — mySuperGroup. Вторая часть — выполнение действий над группами, map.
Фактически всё, что я сделал, сделав решение таким же, как у Вас, это заменил groupBy на filterGroup и добавил дополнительные данные к группе (само действие), чтобы можно было выполнять разные действия над каждой группой.
Здравствуйте, BulatZiganshin, Вы писали:
BZ>-- Функция, которая у других авторов обозначается как |> BZ>-- В общем-то, заменяет нам вызов метода класса BZ>a.$f = f a
В F# конвейерный оператор обозначается как |>
Насчёи Окамля не знаю, не нашёл такого оператора в описании...
Здравствуйте, NotGonnaGetUs, Вы писали:
BZ>>main = do readFile "words" >>== words >>== sort_and_groupOn (take 3) >>>= mapM_ (\list -> writeFile (take 3 (head list)) (unwords list))
NGG>Типа берётся 3-й символ, а не 1-ый и предполагается, что сортировка стабильная (порядок эквивалентых слов сохраняется)?
take 3 xs берёт три первых элемента списка xs, так что сортировка там по первым трём буквам...
Здравствуйте, VoidEx, Вы писали:
VE>В ФП ещё и проще получается. Нету лишнего IFilter, так как IGroup всё равно принимает a, а возаращает Bool, т.е. сама является фильтром. Почему у вас FilteredGroup принимает IProcessor тоже неясно. Группировать фильтры в ФП вообще плёвое дело, просто композиция функций.
Обрати внимание, что у IGroup есть метод done(), а у IFilter — нет и у IProcessor метод add возвращает boolean. Эти интерфейсы реализуют разное поведение, поэтому не могут иметь общего предка (ибо LSP-in-action), даже если бы их сигнатуры были один к одному.
VE>Повторил точь-в-точь ваше решение.
Не совсем или совсем не.
Давай посмотрим:
-- твой же код, но слегка причёсанный (к вопросу о том, что практики принятые в грустном императивном мире полезны не только в нём) type IFilter a = a -> Bool
type IProcessor a b = a -> b
type FilteredGroup a b = (IFilter a, IProcessor a b)
applyFilteredGroups :: [a] -> [FilteredGroup a b] -> [([a], IProcessor a b)]
applyFilteredGroups _ [] = []
applyFilteredGroups items ((gFilter, gProcessor):groups) =
(matchedItems, gProcessor) : (applyFilteredGroups unmatchedItems groups)
where
(matchedItems, unmatchedItems) = partition gFilter items
processGroups :: [a] -> [FilteredGroup a b] -> [[b]]
processGroups d f = map (\x -> map (snd x) (fst x)) $ applyFilteredGroups d f
processGroupsIO d f = mapM_ sequence_ $ processGroups d f
makeStdOutput :: (Show a) => String -> IProcessor a (IO())
makeStdOutput s x = do
putStr $ s ++ ": "
print x
makeByFirstLetter :: Char -> IFilter String
makeByFirstLetter c = isPrefixOf [c]
makeGroupFromChar :: Char -> FilteredGroup String (IO())
makeGroupFromChar c = (makeByFirstLetter c, makeStdOutput [c])
main = processGroupsIO theData $ map makeGroupFromChar ['a'..'z']
Во-первых отсутствует сущность IGroup, т.е. без нарушения OCP нельзя добавить группу не укладывающуюся в рамки FilteredGroup.
Во-вторых IProcessor имеет интерфейс a -> b, а не [a] -> b. Может это и не очень страшно, но чтобы заставить stdOutput печатать в формате 'a -> "abc", "axxx"', а не 'a -> "abc", a -> "axxx"' придётся попрыгать на ровном месте.
В-третьих, поведение FilteredGroup "вшито" в методы processGroups и applyFiltreadGroups — а как быть если нужно слова как-то (в каждой группе по своему) обработать, прежде чем отдавать в процессор или разрешить появление слова в нескольких группах? Создвать методы processGroups1,2,3 с одинаковой сигнатурой (так и до массового копипаста не далеко) или один с кучей параметров (отвёртка на все случаи жизни)? CompositeGroup решал эти проблемы.
В ФП стиле очень легко и красиво решаются задачи с раз и навсегда заданными условиями. Более того, легкость решения позволяет забивать на то, что условия могут слегка меняься — переписать не проблема (вопрос только в масштабируемости такого подхода).
А если к написанию программы подходить с тех же позиций как это делается в оо мире (т.е. обобщить задачу настолько, насколько можно, но не больше — чтобы решение оставалось открытым по отношению к будущим модификациям, но не пыталось все их реализовать сей момент), то разница в количестве строчек кода и его простоты становится уже так уж и велика, а все плюсы скатываются на уровень реализации методов класса (более лаконичная и читаемая форма записи вычислительных алгоритмов) + большее количество иммутабельных структур данных и, как следствие, методов без сайд эффектов (хотя этими же бенефитами (сайд эффекты, иммутабельность) никто не запрещает воспользоваться в императивной программе (это рекомендует делать sun, это есть в скале/немерле, даже idea (java ide) содержит возможность (на радость мне) подсвечивать переменные, которым присваиваются разные значения в ходе жизни метода как ошибочные).
Т.е. плюсы в ФП есть, но сказать, что они кординально изменяют дисциплину программирования, как-то не получается...
NGG>Во-первых отсутствует сущность IGroup, т.е. без нарушения OCP нельзя добавить группу не укладывающуюся в рамки FilteredGroup.
По моему, вы пытаетесь думать в терминах ОО дизайна, что просто неприменимо к ФП.
process (inp filt group_by consume) = inp >>> filter filt >>> group group_by >>> consume
В этом конвейре можно настроить все — и поведение, и данные. И даже сделать так, что бы конкретное поведение определялось структурой данных на входе.
NGG>В ФП стиле очень легко и красиво решаются задачи с раз и навсегда заданными условиями.
Что? Это просто явная неправда. И непонятно, откуда это взялось.
NGG>Более того, легкость решения позволяет забивать на то, что условия могут слегка меняься — переписать не проблема (вопрос только в NGG>масштабируемости такого подхода).
Привести пример, когда ОО-дизайн масштабируется, а ФП — нет. Пока видно, что вы воспринимаете только решение задачи в знакомых терминах, а то, что в незнакомых — просто игнорируете.
NGG>А если к написанию программы подходить с тех же позиций как это делается в оо мире (т.е. обобщить задачу настолько, насколько можно, но не больше — чтобы решение оставалось открытым по отношению к будущим модификациям,
Пример закрытости ФП кода к будущим модификациям, хотя бы на примерах, приведенных выше.
NGG>Т.е. плюсы в ФП есть, но сказать, что они кординально изменяют дисциплину программирования, как-то не получается...
Они совершенно точно требуют другого подхода к дизайну. Читать, например, здесь: Problem K — хороший кейс. Там же по ссылкам или в комментариях можно найти и дизайн на C++ и еще каких-то императивных.
В данном топике задачи слишком простые, что бы к ним серьезно применять термин "дизайн". Problem K — уже гораздо интереснее.
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>Т.е. плюсы в ФП есть, но сказать, что они кординально изменяют дисциплину программирования, как-то не получается...
Извини, но мне кажется у тебя все признаки синдрома "настоящего программиста, который может программировать на любом языке как на ФОРТРАНе", у меня кстати по отношению к ФП тоже присутствует, но гораздо слабее чем у тебя.
NGG>>Во-первых отсутствует сущность IGroup, т.е. без нарушения OCP нельзя добавить группу не укладывающуюся в рамки FilteredGroup.
dmz>По моему, вы пытаетесь думать в терминах ОО дизайна, что просто неприменимо к ФП.
Не совсем или совсем не
Неважно о чём идёт речь о ОО-дизайне, Ф-дизайне или каком-нибудь ещё — "сложность программирования в себе" (вольная трактовка Брукса) заключена в борьбе со сложностью и никуда не исчезает при смене "парадигмы". Способы борьбы со сложностью могут различаться по форме, но по содержанию они не изменны в любой программе. Да, я использую терминологию из ОО-мира, т.к. она широко распространнена и на эти термины можно ссылаться без необходимости тут же раскрывать их значение.
Взять теже шаблоны grasp от лармана. Они остаются актуальны при любом подходе к написанию программ (в отличии от шаблонов GoF, решающих главным образом, проблемы статической типизации).
А если я напишу класс
сlass TaskNumberOne {
void process(Source inp, Filter filt , Group group_by, Consumer consume) {...}
}
вставив внутрь строчки для органиции потока данных по этой цепочке, я тоже разом решу все возможные проблемы?
dmz>В этом конвейре можно настроить все — и поведение, и данные. И даже сделать так, что бы конкретное поведение определялось структурой данных на входе.
Целиком и полностью согласен и нисколько с этим не спорю. Я только повторяю заезженную истину о том, что дьявол в деталях.
Как делается настройка "и поведения, и данных"? Как повторно использовать "труды" настройки?
Если расписать всё это, получатся теже яйца, что и в оо варианте вид сбоку, либо получится решение с другими характеристиками (н-р, очень короткое, но ни разу не используемое повторно, что при определённых условиях тоже может считаться хорошим решением).
NGG>В ФП стиле очень легко и красиво решаются задачи с раз и навсегда заданными условиями. dmz>Что? Это просто явная неправда. И непонятно, откуда это взялось.
Т.е. утверждение, что "очень легко и красиво решаются" неверно?
NGG>>Более того, легкость решения позволяет забивать на то, что условия могут слегка меняься — переписать не проблема (вопрос только в масштабируемости такого подхода).
dmz>Привести пример, когда ОО-дизайн масштабируется, а ФП — нет. dmz>Пример закрытости ФП кода к будущим модификациям, хотя бы на примерах, приведенных выше.
Гхм. Я задал вопросы как будет меняться решение Булата, потыкал в решение VoidEx — чем не примеры?
dmz>Пока видно, что вы воспринимаете только решение задачи в знакомых терминах, а то, что в незнакомых — просто игнорируете.
Как я это делаю? Какой-нибудь пример пожалуйста. Может я и впрямь чего-то не замечаю и не помимаю. Но своё не понимание я пытаюсь выразить в вопросах, ответив на которые вы могли бы его рассеять
dmz>Они совершенно точно требуют другого подхода к дизайну.
Ну, так я и прошу озвучить в чём же этот подход заключается, как при его использовании решаются проблемы борьбы со сложностью озвученные в тех же самых шаблонах грасп и т.п. Так же мне интересно, что мешает применять этот "другой" подход, скажем, лабая код на java. Станет ли он сразу не эффективным и почему? Если не станет, то в чём его плюсы перед имеющимся в наличии ООД? Если плюсы есть, то я стану использовать его в повседневной работе и буду рад безмерно.
dmz>Читать, например, здесь: Problem K — хороший кейс. Там же по ссылкам или в комментариях можно найти и дизайн на C++ и еще каких-то императивных.
).
Там же где-то и ссылка на моё решение на хаскелл...
dmz>В данном топике задачи слишком простые, что бы к ним серьезно применять термин "дизайн". Problem K — уже гораздо интереснее.
Согласен полностью. Простые. Но я же смог показать на простой задаче применение ОО-дизайна. Так же точно, мне думается, не должно быть проблемой продемонстрировать "другой подход к дизайну" на этой же задаче. Некоторые примеры были показаны, я сравнил решения, задал волнующие меня вопросы. Ответы на подобные вопросы по своему решению я могу дать, по чужим — хочу услышать.
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>Во-первых отсутствует сущность IGroup, т.е. без нарушения OCP нельзя добавить группу не укладывающуюся в рамки FilteredGroup.
Далее. NGG>Во-вторых IProcessor имеет интерфейс a -> b, а не [a] -> b. Может это и не очень страшно, но чтобы заставить stdOutput печатать в формате 'a -> "abc", "axxx"', а не 'a -> "abc", a -> "axxx"' придётся попрыгать на ровном месте.
Не подумал. В любом случае — исправляется элементарно, убирается один map, который теперь лежит на плечах самого процессора, делая его гибче. NGG>В-третьих, поведение FilteredGroup "вшито" в методы processGroups и applyFiltreadGroups — а как быть если нужно слова как-то (в каждой группе по своему) обработать, прежде чем отдавать в процессор или разрешить появление слова в нескольких группах? Создвать методы processGroups1,2,3 с одинаковой сигнатурой (так и до массового копипаста не далеко) или один с кучей параметров (отвёртка на все случаи жизни)? CompositeGroup решал эти проблемы.
Не понял. Как сделать функцию, которая принимает на вход процессор и функцию предобработки и возвращает процессор с предобработкой? Вроде как элементарно.
Задача стояла так — сгруппировать неким образом, выполнить над группами действия.
Выделяется группировалка, которая из списка данных делает список групп — туплов со спиком данных в группе и действием над ними.
Таких пишется, например, три штуки:
* Простая, принимающая предикат и операцию, одну на все группы
* Сложная, принимающая список пар фильтров и операций на каждую группу
* Сложная 2, как предыдущая, но элемент попадает во все группы, в которые он подходит
Что данное решение опять не предусмотрело?
Кстати, вопрос к знатокам Хаскеля, как мне из (less) :: a -> a -> Bool простым способом получить a -> a -> Ordering, дабы использовать в sortBy? Я, конечно, не развалюсь написать SortByLess, но может уже готовое есть?
Здравствуйте, VoidEx, Вы писали:
VE>Кстати, вопрос к знатокам Хаскеля, как мне из (less) :: a -> a -> Bool простым способом получить a -> a -> Ordering, дабы использовать в sortBy? Я, конечно, не развалюсь написать SortByLess, но может уже готовое есть?
less2cmp (<) a b | a < b = LT
| b < a = GT
| True = EQ
Здравствуйте, FR, Вы писали:
FR>Извини, но мне кажется у тебя все признаки синдрома "настоящего программиста, который может программировать на любом языке как на ФОРТРАНе", у меня кстати по отношению к ФП тоже присутствует, но гораздо слабее чем у тебя.
Со стороны, как говорится, виднее. Может я где-то увлёкшись и перегибаю палку, но, честное слово, не со зла
Я не пытаюсь программировать "как на фортране". Просто программирование "на фортране" привело к тому, что в голове всегда находятся вопросы относящиеся к качеству кода, формулируемые без относительно к языку на котором код пишется. Поэтому когда я слышу слова о том, что использование ФЯ приводит к качественному коду, мне, естественно, хочется услышать так же, а каким образом достигается качество, чем методы достижения качества отличаются от знакомых мне по грустному опыту программерской жизни методов.
Интерес у меня тут практический.
Вот недавно, повинуясь тому же интересу, получил массу удовльствия читая у окасаки про рандом ассесс листы и кучи и их связь с представлением чисел. Очень лакомый кусочек эти не изменяемые структуры данных, в работе пригодятся
Может и в этой теме что-нибудь полезное получу для себя в итоге.
Здравствуйте, BulatZiganshin, Вы писали:
BZ>Здравствуйте, VoidEx, Вы писали:
VE>>Кстати, вопрос к знатокам Хаскеля, как мне из (less) :: a -> a -> Bool простым способом получить a -> a -> Ordering, дабы использовать в sortBy? Я, конечно, не развалюсь написать SortByLess, но может уже готовое есть? BZ>
BZ>less2cmp (<) a b | a < b = LT
BZ> | b < a = GT
BZ> | True = EQ
BZ>
BZ>вроде не бином Ньютона
Я потому и написал, что не разваюсь, но думал, что такая полезная функция и стандартной могла бы быть
Здравствуйте, geniepro, Вы писали:
G>Здравствуйте, VoidEx, Вы писали:
VE>>... как мне из (less) :: a -> a -> Bool простым способом получить a -> a -> Ordering, дабы использовать в sortBy?
G>Если под less имеется в виду less then, то, наверное, типа такого: G>
lt a b = if less a b then LT else EQ
или
lt a b = if less a b then LT else GT
G>и вот возникает вопрос, что же предпочесть -- EQ или GT? У compare ведь три возможных значения, а у операции < -- всего два...
Ну зато операцию < можно развернуть в обратную сторону, тогда можно получить все три варианта
Здравствуйте, geniepro, Вы писали:
G>Здравствуйте, VoidEx, Вы писали:
VE>>... как мне из (less) :: a -> a -> Bool простым способом получить a -> a -> Ordering, дабы использовать в sortBy?
G>Если под less имеется в виду less then, то, наверное, типа такого:
Наверное всёж less than, хотя ошибка даж в яндексовые словари попала (сборник Бродского на самом деле называется "Less than one").
P.S. Я знаю, что зануда
Здравствуйте, VoidEx, Вы писали:
NGG>>В-третьих, поведение FilteredGroup "вшито" в методы processGroups и applyFiltreadGroups — а как быть если нужно слова как-то (в каждой группе по своему) обработать, прежде чем отдавать в процессор или разрешить появление слова в нескольких группах? Создвать методы processGroups1,2,3 с одинаковой сигнатурой (так и до массового копипаста не далеко) или один с кучей параметров (отвёртка на все случаи жизни)? CompositeGroup решал эти проблемы. VE>Не понял. Как сделать функцию, которая принимает на вход процессор и функцию предобработки и возвращает процессор с предобработкой? Вроде как элементарно.
Речь шла о том, что
processGroups :: [a] -> [FilteredGroup a b] -> [b]]
может обрабатывать только список из FilteredGroup. Причём делает это раз и на всегда заданным образом.
А если между этих групп нужна такая, которая забирает все слова образующие цепочку (берёт первое слово, а затем все первая буква которого, начинается с последней буквы предыдушего) и в результате обработки выплёвывает в файл с именем равным последнему слову суммарное количество слов в группе и максимальную длина цепочки набранной из подряд идущих слов?
Для данной задачи это не беда, можно быстренько переписать решение. Но не переписывать же его на каждый чих? Хочется как-то гарантировать стойкость к изменениям...
VE>Задача стояла так — сгруппировать неким образом, выполнить над группами действия. VE>Выделяется группировалка, которая из списка данных делает список групп — туплов со спиком данных в группе и действием над ними. VE>Таких пишется, например, три штуки: VE> * Простая, принимающая предикат и операцию, одну на все группы VE> * Сложная, принимающая список пар фильтров и операций на каждую группу VE> * Сложная 2, как предыдущая, но элемент попадает во все группы, в которые он подходит VE>Что данное решение опять не предусмотрело?
Хитрожопую группу, которую простым фильтром не получить Н-р, нужно помнить состояние, а это меняет интерфейс группировалки...
Понятно, что "хорошее" решение, к которому я не смог бы придраться существует. Интересен принцип по которым оно строилось бы с полпинка.
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>>>Может кто-нибудь из "гуру" набросать, как будет выглядеть "обобщённое" решение 1-ой задачки в функциональном стиле?
BZ>>просто реализовываешь sort_and_groupOn и дальше отдаёшь ей какие тебе нужно предикаты, например
BZ>>main = do readFile "words" >>== words >>== sort_and_groupOn (take 3) >>>= mapM_ (\list -> writeFile (take 3 (head list)) (unwords list))
NGG>А как изменится решение, если вместо (take 3) использовать (contains 'a'), (contains 'b')... и т.д.? Т.е. одно и тоже слово может попадать в разные файлы? И как тогда определить имя файла?
написать функцию, отображающую список предикатов плюс список значений в список подсписков:
split2groups :: [a->Bool] -> [a] -> [a]]
с нею решение будет выглядеть так:
main = do readFile "words" >>== words >>== split2groups (map contains ['a'..'z']) >>== zip ["a".."z"]
>= mapM_ (\filename list -> writeFile filename (unwords list))
NGG>Собственно эти вопросы и тревожат. NGG>С одной стороны, когда решение это две строчки, его можно и заново написать, если требования слегка изменятся. NGG>С другой сторы этот подход не маштабируется "на многострочное решение" или особо хитрые ребусы.
в каком смысле не масштабируется?
NGG>Угу, в плане "покомбинировать" ФП молодец. Особенно пока можно обойсь "функтором", а не интерфейсом содержащим несколько методов и пока не появились интерфейсы содержащие одинаковые методы, но с разными контрактами...
несколько методов передать можно точно так же. насчёт контрактов я не понял — дай пример
NGG>"не боясь запутаться" — это, как мне кажется, относится только к "чистым" функциям без сайд эффектов и всяких lazy-хитростей. Иначе всё равно буду бояться
я сужу по своему опыту. а ты судишь по отсутствию опыта
BZ>>таким образом, пропадает необходимость описывать монолитный класс, включающий всю необходимую тебе функциональность — порграмма становится более модульной, каждая функция решает ровно одну задачу
NGG>Допустим, что при использовании функций как первокласных сущностей необходимость описывать монолитный класс пропадает, а разве откуда-то вообще следует необходимость описывать монолитный класс (н-р, при той же оо-декомпозиции)?
а почему моё решение комбинирует несколько функций для получения нужного результата, а твоё — монолитно? я думаю, причина в том, что в C* описание класса или даже функции — достаточно многословно, поэтому есть тенденция складывать в них больше функциональности, нежели в хаскеле
NGG>Взять теже шаблоны grasp от лармана. Они остаются актуальны при любом подходе к написанию программ (в отличии от шаблонов GoF, решающих главным образом, проблемы статической типизации).
Боюсь мы уходим в некие обобщения, обсуждать справедливость которых я не готов.
dmz>>
NGG>А если я напишу класс NGG>сlass TaskNumberOne { NGG>void process(Source inp, Filter filt , Group group_by, Consumer consume) {...} NGG>}
NGG>вставив внутрь строчки для органиции потока данных по этой цепочке, я тоже разом решу все возможные проблемы?
Все возможные проблемы решить вообще невозможно. Эту задачу — да, вполне можно решить и так. На джаве так обычно не делают — для нее это будет непривычный дизайн. Но вполне можно. А можно применить шаблон template method, например, а конкретные реализации параметризовать. Получится по смыслу почти то-же, что и в ФП примере.
В случае джавы контракты между inp, filter, group_by (кстати group by недостаточно, если нет group — т.к. стандартного group с известным поведением нет — а в ФП это один из базовых примитивов, тоже касается filt) определятся интерфейсами, в ФП — типами.
NGG>Как делается настройка "и поведения, и данных"? Как повторно использовать "труды" настройки?
1) передать новые реализации соответствующих методов
2) определить типы, для которых определены соответствующие реализации. Бочки фильтруем и группируем — так, а ящики — сяк.
3) определить функции, в которые уже подставлены какие-то параметры, которые скорее всего не будут меняться.
Но это все пустое — почему, см. ниже (т.к. решается не та задача)
Очевидно, что у нас есть повторно используемые компоненты — источник (input) — который генерирует последовательность сущностей, filter, группирующая функция (считает некий ключ для этих сущностей), и консьюмер, да и сам алгоритм, в общем. Который в силу полиморфизма можно использовать для других объектов, для которых определены соответствующие компоненты.
NGG>Если расписать всё это, получатся теже яйца, что и в оо варианте вид сбоку,
Да что-то вот никак не получаются.
NGG>либо получится решение с другими характеристиками (н-р, очень короткое, но ни разу не используемое повторно, что при определённых условиях тоже может считаться хорошим решением).
Да, именно. Сложное, в угоду "предположительно" повторному использованию решение простой задачи — это плохо.
NGG>Т.е. утверждение, что "очень легко и красиво решаются" неверно?
Очевидно, что неверно про раз и навсегда заданные задачи.
NGG>Гхм. Я задал вопросы как будет меняться решение Булата, потыкал в решение VoidEx — чем не примеры?
Ну сейчас ознакомлюсь более детально, хотя, конечно начинает утомлять.
dmz>>Они совершенно точно требуют другого подхода к дизайну. NGG>Ну, так я и прошу озвучить в чём же этот подход заключается, как при его использовании решаются проблемы борьбы со сложностью озвученные в тех же
NGG>самых шаблонах грасп и т.п. Так же мне интересно, что мешает применять этот "другой" подход, скажем, лабая код на java. Станет ли он сразу не NGG>эффективным и почему? Если не станет,
Да потому, что язык не поддерживает. Честно говоря, приплетать опять грасп к этой задаче — это уже слишком. Ну вот пример — был у нас фильтр непустых строк, но, допустим, надо бы отсортировать комментарии и вообще — уметь наложить вместо одного фильтра N фильтров, но фильтры известны в компайл-тайме. Ну, в общем, вполне себе допущение, не хуже уже прочих.
Решение на ОО (оппа, группового наложения мы сразу не не предусмотрели? Ну, не беда)
Решение адын:
class FilterA : Filter { десять строк }
class FilterB : Filter { десять строк }
class FilterB : Filter { десять строк }
class MegaFilter : Filter
{
bool apply( Entity entity ) {
return FilterA(entity) && FilterB(entity) && FilterC(entity)
}
};
Решение два:
class MegaFilter : Filter
{
MegaFilter( List<Filter>& filt ) {}
bool apply( Entity entity ) {
foreach( ... ) { ... }
}
};
Решение три - рефакторим интерфейсы
NGG>то в чём его плюсы перед имеющимся в наличии ООД? Если плюсы есть, то я стану использовать его в повседневной работе и буду рад безмерно. NGG>Согласен полностью. Простые. Но я же смог показать на простой задаче применение ОО-дизайна.
В итоге имеем овердизайн. Задача, которую решаем решена излишне сложно, построен некий фреймворк "для решения подобных задач", при этом полнота его не вполне известна; более того, из постановки очевидно, что "принять набор сущностей, отфильтровать, сгруппировать и и отдать консьюмеру" — это не есть сама задача — это решение некоей задачи. Соответственно, бессмысленно обобщать эту задачу — надо обобщать, и делать некие предположения относительно исходной задачи. В итоге решена некая задача, необходимость решения которой неочевидна. Потрачено время. Съехали оценки времени — т.е. ставя задачу считать, отсортировать и построить индекс (если это задача построения некоего индекса) — я никак не ожидаю увидеть в итоге нечто подобное тому, что увидел. Более того, если это была именно задача построения индекса — то, например, задача передачи его по TCP о которой тут тоже задумались — не возникнет никогда ВООБЩЕ, т.е. даже 15 минут тратить на рассмотрение этого бессмысленно. Далее, если задача решается в одну-две строки — то очевидно, если ВДРУГ, когда нибудь, в следующей жизни потребуются некоторые модификации, то две строчки проще выкинуть и написать заново, если уж на то пошло. Более того, человек, который написал мега-фреймворк для сортировки ящиков, может уже не работать на момент, когда изменения понадобятся. Придет следующий человек. Получит задачу сделать так, что бы помимо ящиков, сортировались так же и бочки. Увидит фреймворк. Как думаете, он будет вникать в весь "повторно использованный код" ? Или проведет "рефакторинг" — т.е. выкинет или забьет, и напишет свое?
Ну и? Время потрачено , код bloated, больше кода — больше поддержки и документирования, больше время на въезжание новым членам команды, в общем, все при деле. Особенно хорошо становится, когда креативных людей больше одного — все читали гамму, лармана и фаулера, там столько паттернов...
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>Но если решается "реальная" задача (а не игрушечная), имеет смысл обобщить слегка задачу, чтобы перекрыть себе тылы на случай изменений в требованиях. Поэтому я привёл обобщённый вариант нашей игрушечной задачи и его решение в оо-стиле. Это тоже игра Смысл игры в том, чтобы показать как "тупое" ооп может привести к многословному, но достаточно гибкому решению.
возможно, тебя не понимают потому, что обобщение в ФП — самая тривиальная операция, не требующая какой-то специальной методологии и пр. если обобщаемая часть алгоритма является синтаксически законченной, то ты просто превращаешь её в параметр:
BZ>>>main = do readFile "words" >>== words >>== sort_and_groupOn (take 3)
>>>>= mapM_ (\list -> writeFile (take 3 (head list)) (unwords list))
BZ>main = do readFile "words" >>== words >>== split2groups (map contains ['a'..'z']) >>== zip ["a".."z"]
>>= mapM_ (\filename list -> writeFile filename (unwords list))
BZ>
NGG>>С одной стороны, когда решение это две строчки, его можно и заново написать, если требования слегка изменятся.
Угу, т.е. решение просто переписывается, как раз об этом я и писал.
Поскольку задача решалась "в одну условную строчку" это сделать просто и вопросов никаких нет.
NGG>>С другой сторы этот подход не маштабируется "на многострочное решение" или особо хитрые ребусы. BZ>в каком смысле не масштабируется?
В том смысле, что старое решение вон и пишем новое...
А если бы задачка была не тривальная? Разве не стоит "подстелить соломку" заранее, чтобы потом было мягче падать?
BZ>несколько методов передать можно точно так же. насчёт контрактов я не понял — дай пример
Пусть у нас есть функции:
a :: Int -> Strig
b :: String->Int
a':: Int -> Strig
b':: String->Int
с:: (Int -> String) -> (String -> Int) -> String(неважно что) -> String (в неважно что)
Контракт функции с — первый и второй аргументы это прямое и обратное преоразование. Внутри функции — алгоритм которому важно выполнение этих свойств. Пары a,b и a',b' удовлетворяют этим контрактам, a,b' и a',b — нет.
Использование функции c не безопасно — н-р, можно при правке кода поменять a на a', а про b забыть...
Компилятор промолчит. Может быть даже тесты пройдут успешно, т.к. разница между b и b' крылась в каком-то редком случае почему-то не покрытому при тестировании. А в итоге приложение не отработает как следует в самый ответственный момент.
Возможно моя боязнь блуждающих по аргументам в свободном полёте связных функций это только от недостатка опыта в их готовке...
Но пока мне кажется, что разумно было бы явно декларировать зависимость между аргументами:
data Converter = Converter
{
encoder :: (Int->String),
decoder :: (String -> Int)
}
c :: Converter -> String(неважно что) -> String (в неважно что)
Из декларации сразу видно что к чему.
Разве нет?
(Конечно, приверженцы динамической типизации дружно пошлют меня в сад (пусть, не здесь с ними спорить ), но хаскелл всё-таки язык со статической типизацией.)
BZ>я сужу по своему опыту. а ты судишь по отсутствию опыта
Подколол
BZ>а почему моё решение комбинирует несколько функций для получения нужного результата, а твоё — монолитно? я думаю, причина в том, что в C* описание класса или даже функции — достаточно многословно, поэтому есть тенденция складывать в них больше функциональности, нежели в хаскеле
Гхм. Тут какое-то расхождение во взглядах на то, что такое монолитность
Мне как раз твоё решение видится монолитным кусоком кода, а моё комбинируется из нескольких классов для полученя нужного результата и каждый из этих классов может жить счастливой самостоятельной жизнью.
Само-то решение вот:
//Создание фильтров
ArrayList<IGroup<String>> groups = new ArrayList<IGroup<String>>();
for (char letter = 'a'; letter <= 'z'; letter++) //да, заменить for на map было бы приятно... но суть не меняется.
{
groups.add(new FilteredGroup<String>(new FileProcessor(Character.toString(letter)),
new ByFirstLetter(letter)));
}
//Задание приоритетов
IGroup<String> group = new CompositeSeqGroup<String>(groups);
//обработка
//место этого цикла, как я уже говорил, не правильное. он должен жить в дефолтной реализации метода IGroup#addAll()for (int i = 0; i < args.length; i++)
{
group.add(args[i]);
}
group.done();
Здравствуйте, dmz, Вы писали:
dmz>Более того, человек, который написал мега-фреймворк для сортировки ящиков, может уже не работать на момент, когда изменения понадобятся. Придет следующий человек. Получит задачу сделать так, что бы помимо ящиков, сортировались так же и бочки. Увидит фреймворк. Как думаете, он будет вникать в весь "повторно использованный код" ? Или проведет "рефакторинг" — т.е. выкинет или забьет, и напишет свое?
Вы не так поняли, точнее смотрите с другой точки зрения. Только с позиции одного разработчика. Фича мега-фреймворка как раз в том, что какую-то часть, например, сортировку ящиков может делать один человек, а другой может писать сортировку бочек. Причем им нужен только интерфейс ISort.
Lisp is not dead. It’s just the URL that has changed: http://clojure.org
NGG>При этом в моих руках остаётся возможность менять политики обработки (как и когда сбрасывать данные в файл или слать в сокет и т.п.), можно заменить CompositeSeqGroup на AsyncCompositeGroup так, что группы не будут ждать друг-друга...
Слать это в сокет очень полезно, без сомнения. А вот сделаем реалистичное допущение относительно данной задачи в рамках ее предметной области (уж какая есть) — писать в каждый файл только уникальные строки (т.к. на кой черт писать дублирующие?)
решение на условном ФП:
(* было *)
do_job_with input filt group_by consumer = input >>> filter filt >>> group group_by >> consumer
(* да, надо было параметризовать фильтрацию. ну ладно - рефакторим *)
do_job_with input filter group_by consumer = input >>> filter >>> group group_by >>> consumer
...
concrete_filter = unique . filter filt
...
dmz>>В этом конвейре можно настроить все — и поведение, и данные. И даже сделать так, что бы конкретное поведение определялось структурой данных на входе.
NGG>Целиком и полностью согласен и нисколько с этим не спорю. Я только повторяю заезженную истину о том, что дьявол в деталях. NGG>Как делается настройка "и поведения, и данных"?
передачей их в качестве параметров
NGG>Как повторно использовать "труды" настройки?
видимо, речь идёт о partial application?
NGG>А если я напишу класс NGG>сlass TaskNumberOne { NGG>void process(Source inp, Filter filt , Group group_by, Consumer consume) {...} NGG>} NGG>вставив внутрь строчки для органиции потока данных по этой цепочке, я тоже разом решу все возможные проблемы?
да, а разве нет? разница только в том, что на C# это наверно будет достаточно сложная функция
NGG>Ну, так я и прошу озвучить в чём же этот подход заключается, как при его использовании решаются проблемы борьбы со сложностью озвученные в тех же самых шаблонах грасп и т.п. Так же мне интересно, что мешает применять этот "другой" подход, скажем, лабая код на java. Станет ли он сразу не эффективным и почему? Если не станет, то в чём его плюсы перед имеющимся в наличии ООД? Если плюсы есть, то я стану использовать его в повседневной работе и буду рад безмерно.
все три C* языка активно добавляют в свой арсенал ФП фичи. поэтому давайте посмотрим скажем какие проблемы возникнут при использовании ФП подхода в чистом С или C++ 80-х годов:
1) нам нужна передача функций в качестве параметров и возвращение их в качестве результатов. это поддерживается
2) нужна поддержка анонимных лямбд. этого нет — все функции должны быть описаны глобально. но по крайней мере выразительную мощь языка это не изменяет
3) нужна поддержка частичного применения. этого нет
4) нужен двухсторонний автовывод типов с автоматическим обобщением. этого тоже нет, что затрудняет разбиение алгоритмов на множество небольших [глобальных, локальных и анонимных] функций — засребёшься описывать типы всех их параметров и результатов
5) нужно ленивое вычисление. тоже нет
6) ну и самом собой, нужен GC
в остальном ФП подходы вполне применимы и на C* языках
dmz>>В данном топике задачи слишком простые, что бы к ним серьезно применять термин "дизайн". Problem K — уже гораздо интереснее. NGG>Согласен полностью. Простые. Но я же смог показать на простой задаче применение ОО-дизайна. Так же точно, мне думается, не должно быть проблемой продемонстрировать "другой подход к дизайну" на этой же задаче. Некоторые примеры были показаны, я сравнил решения, задал волнующие меня вопросы. Ответы на подобные вопросы по своему решению я могу дать, по чужим — хочу услышать.
моё понимание ФП дизайна — это представить себе, что ты для каждой задачи разрабатываешь свой собственный язык программирования. ты не ограничен определённым набором средств структурирования программы, типа классов или указателей. твоя задача — объяснить компьютеру как решить проблему, используя при этом любой удобный тебе алгоритмический язык. и тут на первое место выходит именно то, как сделать это объяснение наиболее читабельным и сопровождаемым
вот пример. представим классическую gui-библиотеку. реализация диалога настроек на ней может выглядеть так:
1) создаём все необходимые controls
2) заполняем их исходными данными
3) отображаем диалог
4) после нажатия Ok обновляем переменные
удобно ли это? на мой взгляд нет — нужно сгруппировать описание контрола, перевод данных в него и обратно. соответственно ФП-дизайн здесь состоит в создании высокоуровневых контролов, которые несут в себе не только свои данные, но и ассоциированные с ними действия. и тогда программа будет выглядеть так:
Здравствуйте, yumi, Вы писали:
Y>Здравствуйте, dmz, Вы писали:
dmz>>Более того, человек, который написал мега-фреймворк для сортировки ящиков, может уже не работать на момент, когда изменения понадобятся. Придет следующий человек. Получит задачу сделать так, что бы помимо ящиков, сортировались так же и бочки. Увидит фреймворк. Как думаете, он будет вникать в весь "повторно использованный код" ? Или проведет "рефакторинг" — т.е. выкинет или забьет, и напишет свое?
Y>Вы не так поняли, точнее смотрите с другой точки зрения. Только с позиции одного разработчика. Фича мега-фреймворка как раз в том, что какую-то часть, например, сортировку ящиков может делать один человек, а другой может писать сортировку бочек. Причем им нужен только интерфейс ISort.
Тема сортировки пока никем не поднималась, но в рамках предметной области допущение, что она понадобится — вполне резонное. ISort — это, кстати, у кого будет такой интерфейс? В рамках предложенной декомпозиции? Ась?
Но смотрю я не с точки зрения разработчика. Смотрю я, допустим, с точки зрения заказчика или аутсорс-менеджера. И вижу, что вместо задачи построения дискового индекса, которую я заказывал, мне впаривают фреймворк для (цитирую)
[quote]
Дано: источник данных, критерий разбиения данных на группы
Надо: данные разбить на группы, обработать данные в группах.
[/quote]
Таких задач не бывает, но да ладно. Для увеличения вероятности того, что нам такая задача понадобится — расширим ее аудиторию с рамок данного проекта до всего мира. Выложим в сеть. Как будете искать ее решение в гугле? Ладно, допустим, мы нашли.
Что мы видим? Для того, что бы данным решением воспользоваться — нужно пользоваться контейнерным (по сути) классом Group (если у нас данные ходят в каком-нибудь обычном List — придется написать адаптер, какой — нибудь свой FromListProcessor, разобраться, чем отличаются Group от Processor с одинаковым интерфейсом (две разные сущности с внешне одинаковым поведением, тесно сцепленные), а потом еще вытащить свои данные из Group.
И это вместо того, что бы тупо в лоб воспользоваться фундаментальными примитивами group, map, filter — которые будут работать с любыми списками (не знаю, как в джаве, но в питоне есть group, filter, map прямо вот в таком виде) — на которые данная задача естественно декомпозируется.
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>Гхм. Тут какое-то расхождение во взглядах на то, что такое монолитность NGG>Мне как раз твоё решение видится монолитным кусоком кода, а моё комбинируется из нескольких классов для полученя нужного результата и каждый из этих классов может жить счастливой самостоятельной жизнью.
Так как в функциональных языках функция первоклассная сущность, твое высказывание перепишется так:
а моё комбинируется из нескольких функций для полученя нужного результата и каждая из этих функций может жить счастливой самостоятельной жизнью.
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>Хитрожопую группу, которую простым фильтром не получить Н-р, нужно помнить состояние, а это меняет интерфейс группировалки...
NGG>Понятно, что "хорошее" решение, к которому я не смог бы придраться существует. Интересен принцип по которым оно строилось бы с полпинка.
На самом деле непонятно. Потому что появляются такие усложнения, которые скорее говорят о кривом решении некой другой задачи, раз появилась такая, где для группировки скоро придётся в базу данных обращаться, причём внутри самой группировалки.
Т.е. если вот приспичило цепочку собрать, так можно её и собрать, написав элементарную функцию, а не писать обобщённую группировалку, которая бы умела всё.
type Group a b = [a] -> ([([a], [a] -> b)], [a])
passFirst g s = g s
passAll g s = (fst $ g s, s)
makeGroup :: (Eq a) => ([a] -> [a]) -> ([a] -> b) -> Group a b
makeGroup f p l = ([(m, p)], l \\ m) where m = f l
composeGroup p f n [] = ([], [])
composeGroup p f n l = (fg ++ ng, ntail) where
(fg, ftail) = p f l
(ng, ntail) = p n ftail
seqGroup :: (Eq a) => Group a b -> Group a b -> Group a b
seqGroup = composeGroup passFirst
unionGroup :: (Eq a) => Group a b -> Group a b -> Group a b
unionGroup = composeGroup passAll
groupAndProcess input grouping = mapM_ (\x -> (snd x) (fst x)) $ fst $ grouping input
-- Остальное ниже - семечки, к делу не относящиеся
groupCmp cmp [] = []
groupCmp cmp l@(s:_) = filter (cmp s) l
groupByWith cmp x = seqGroup (makeGroup (groupCmp cmp) x) (groupByWith cmp x)
consoleOutput prefix input = putStrLn prefix >> mapM_ (putStrLn . ("\t" ++)) input
byFirstLetter :: String -> String -> Bool
byFirstLetter _ [] = False
byFirstLetter [] _ = False
byFirstLetter (l:_) (r:_) = l == r
groupStated f i l = map fst $ filter snd $ zip l $ fst $ runState (mapM f l) i
statedFilter :: String -> State String Bool -- группировалка со стейтом
statedFilter s = do
lastStr <- get
if ((null lastStr) || (null s) || ((last lastStr) == head s))
then do
put s
return True
else return False
input = ["abc", "cde", "efg", "aaa", "ghi", "sxdf", "zadedf", "sfsfg", "atrhyh", "43f5", "sdcsby", "Unuufd"]
main = groupAndProcess input $ unionGroup -- union, т.е. элементы попадут в обе группы
(makeGroup (groupStated statedFilter "") (\xs@(x:_) -> consoleOutput [head x] xs)) -- составляем цепочку слов
(groupByWith byFirstLetter (\xs@(x:_) -> consoleOutput [head x] xs)) -- группируем по первому символу
Смотря на всё это, делаю 3 вывода: 1. Основной код всё равно невелик, 15 строк
2. Либо всё это пишется проще на порядок (хотелось бы тогда от гуру увидеть подтверждение)
3. И проще решать частные задачи по 2 строки на каждую, чем делать такое обобщение, которым и пользоваться не будет никто
Собственно, то же самое, но в частном случае:
byFirstLetter :: String -> String -> Bool
byFirstLetter _ [] = False
byFirstLetter [] _ = False
byFirstLetter (l:_) (r:_) = l == r
makeChain strs = reverse $ makeChain' strs "" [] where
makeChain' [] cur acc = acc
makeChain' (c:cs) cur acc
| isPrefixOf cur c = makeChain' cs [last c] (c:acc)
| otherwise = makeChain' cs cur acc
main = consoleOutput "chain" chain >> mapM_ (\xs@(x:_) -> consoleOutput [head x] xs) groups where
(chain, groups) = (makeChain &&& (groupBy byFirstLetter . sort)) input
BZ>>несколько методов передать можно точно так же. насчёт контрактов я не понял — дай пример
и чем тебя не устраивает твоё решение?
BZ>>а почему моё решение комбинирует несколько функций для получения нужного результата, а твоё — монолитно? я думаю, причина в том, что в C* описание класса или даже функции — достаточно многословно, поэтому есть тенденция складывать в них больше функциональности, нежели в хаскеле
NGG>Гхм. Тут какое-то расхождение во взглядах на то, что такое монолитность NGG>Мне как раз твоё решение видится монолитным кусоком кода, а моё комбинируется из нескольких классов для полученя нужного результата и каждый из этих классов может жить счастливой самостоятельной жизнью.
NGG>Само-то решение вот: NGG>Где тут монолитность?
виноват, я в твоём решении просто не разбирался. имхо, оно ничем не отличается от моего, только я ес-но использую функции, а не классы. если к примеру тебе потребуется разбивать на классы 0..9 вместо a..z, то точно также придётся менять процитированный тобой код, как и в моём решении
Здравствуйте, dmz, Вы писали:
NGG>>вставив внутрь строчки для органиции потока данных по этой цепочке, я тоже разом решу все возможные проблемы? dmz>Все возможные проблемы решить вообще невозможно. Эту задачу — да, вполне можно решить и так.
Ну так если затем расписать все перечисленные в аргуметах интерфейсы классы, получится тоже самое количество кода что в обрисованном мной вараинте.
dmz>А можно применить шаблон template method, например, а конкретные реализации параметризовать. Получится по смыслу почти то-же, что и в ФП примере.
Так оно влюбом варианте получится тоже самое по смыслу...
dmz>Очевидно, что у нас есть повторно используемые компоненты — источник (input) — который генерирует последовательность сущностей, filter, группирующая функция (считает некий ключ для этих сущностей), и консьюмер, да и сам алгоритм, в общем. Который в силу полиморфизма можно использовать для других объектов, для которых определены соответствующие компоненты.
+ NGG>>Если расписать всё это, получатся теже яйца, что и в оо варианте вид сбоку
IFilter — filter
IGroup#add — группирующая функция (не один к одному, т.к. есть разница между группами заданными из вне и группа создаваемыми в ходе работы алгоритма (как в решении с (take 3)). "Знание" о таких группах находится в реализации CompositeGroup и там может и должен использовать дескриминатор строящий по данным группу)
IProcessor — консьюмер
СompositeGroup — сам алгоритм
Разве не яйца?
dmz>Очевидно, что неверно про раз и навсегда заданные задачи.
Наверное, очевидным должно быть, что не только для раз и на всегда заданных условий, а для более широго круга задач
Вот собственно поддтверждения этому я и хочу найти.
NGG>>Ну, так я и прошу озвучить в чём же этот подход заключается, ... Станет ли он сразу не эффективным и почему? dmz>Да потому, что язык не поддерживает.
А что он должен поддреживать? Только анонимные функции? Т.е. в C# уже на ура будет работать?
dmz>Честно говоря, приплетать опять грасп к этой задаче — это уже слишком.
Они приплетались не к этой задаче, а к вопросу о том с каким самоваром я лезу в фп
Куда, н-р, денешься от того, что есть точки не устойчивости в программах? Надо же как-то с этим бороться. В ООП интерфейсом отгараживаются, в ФП тоже как-то нужно бороться.
dmz> Ну вот пример — был у нас фильтр непустых строк, но, допустим, надо бы отсортировать комментарии и вообще — уметь наложить вместо одного фильтра N фильтров, но фильтры известны в компайл-тайме. Ну, в общем, вполне себе допущение, не хуже уже прочих.
dmz>Решение на ОО (оппа, группового наложения мы сразу не не предусмотрели? Ну, не беда) dmz>
dmz>class MegaFilter : Filter
dmz>{
dmz>};
dmz>
Гхм. Это вообще-то называется AndFilter и его добавление не приводит к изменниям в коде. Тот самый OCP в действии
Реализовывать его, пока он не был нужен и было бы овердизайном и заточкой под изменения, которые никогда не случатся (в отличии от декларации интерфейсов).
dmz>Решение три — рефакторим интерфейсы
Давайте без этого
NGG>>то в чём его плюсы перед имеющимся в наличии ООД? Если плюсы есть, то я стану использовать его в повседневной работе и буду рад безмерно. NGG>>Согласен полностью. Простые. Но я же смог показать на простой задаче применение ОО-дизайна. dmz>В итоге имеем овердизайн.
+1024.
Только цель предложенного дизайна была не показать как ооп крут, а фп гавно (такое ощущение, что многими, кто мне отвечал, оно воспринимается именно так). В том сообщении я задал вопрос про то, как выглядело бы решение в функциональном стиле будь это не задачка на две строчки, а "реальная" задача. ОО-код служит примером того, как я ответил бы на этот вопрос задай мне его кто-нибудь. Просто иллюстрация, чтобы было понятно о чём я спрашиваю. Похоже канал коммуникации этим приёмом настроить не удалось
dmz> из постановки очевидно, что "принять набор сущностей, отфильтровать, сгруппировать и и отдать консьюмеру" — это не есть сама задача — это решение некоей задачи.
Да как сказать... не очень очевидно
dmz> если задача решается в одну-две строки — то очевидно, если ВДРУГ, когда нибудь, в следующей жизни потребуются некоторые модификации, то две строчки проще выкинуть и написать заново, если уж на то пошло.
Опять +1024, но я уже тоже об этом раз 5 упомянул в своих ответах.
dmz> Как думаете, он будет вникать в весь "повторно использованный код" ? Или проведет "рефакторинг" — т.е. выкинет или забьет, и напишет свое?
Оффтоп:
Вот я сейчас в роли такого человека оказался. Перед глазами три "фреймоврка" для "легкого" построения гуя в одном приложение и все настолько безобразны (7+ лет приложению), что я плачу. Уже давно мечтаю всё выкинуть и написать своё, бо цена рефакторинга почти такая же, но если текущая задача стоит Х, переписать и выполнить текущую задачу "красиво" будет стоить 100*X, если не больше. Приходится закрывать глаза и получать удовльствие
Собственно эта ситуация отчасти поспособствовала тому, что я стал задавать тут глупые вопросы. Стало интересно, что было бы, пиши это приложение в течении 7 лет теже недалёкие люди, но только на функциональном языке. Больше гавна пришлось бы разбирать или меньше?
dmz>Особенно хорошо становится, когда креативных людей больше одного — все читали гамму, лармана и фаулера, там столько паттернов...
Я боюсь, что "креативных людей" в оо-мире не больше, чем было бы в ф-мире стань он мейнстримом, только читали бы не эти книги, а всяких других умных людей
dmz>Я еще раскрою тему.
Жду с нетерпением
Здравствуйте, dmz, Вы писали:
dmz>Но смотрю я не с точки зрения разработчика. Смотрю я, допустим, с точки зрения заказчика или аутсорс-менеджера. И вижу, что вместо задачи построения дискового индекса, которую я заказывал, мне впаривают фреймворк для (цитирую)
Вы передёргиваете. В данном случае, и заказчик и исполнитель я. И таких требований у меня нет.
Принять решение насколько нужно обобщать задачу без общения с заказчиком программист не должен. Это вне рамок его компетенции.
dmz>Что мы видим? Для того, что бы данным решением воспользоваться — нужно пользоваться контейнерным (по сути) классом Group (если у нас данные ходят в каком-нибудь обычном List — придется написать адаптер, какой — нибудь свой FromListProcessor, разобраться, чем отличаются Group от Processor с одинаковым интерфейсом (две разные сущности с внешне одинаковым поведением, тесно сцепленные), а потом еще вытащить свои данные из Group.
Неа, нужно будет только написать свой процессор, который будет результат представлять ввиде желаемого вам листа или кренделя с бубликом.
И никаких гвоздей.
К моменту завоевания мира эти реализации уже будет в составе фреймворка
dmz>И это вместо того, что бы тупо в лоб воспользоваться фундаментальными примитивами group, map, filter — которые будут работать с любыми списками (не знаю, как в джаве, но в питоне есть group, filter, map прямо вот в таком виде) — на которые данная задача естественно декомпозируется.
Кстати, да. В отличии от того же c# в java стандартных group,filter,map — нет. Писать самоделки не даёт отсутствие "компактного" синтаксиса для написания лямбд и понижение производительности связанное с копирование списков, что в сумме перевешивает выгоды от наличия таких методов. В 7-ой будут лямбы и, думается мне, обновятся соответствующим образом стандартные библиотеки.
Здравствуйте, BulatZiganshin, Вы писали:
NGG>>Целиком и полностью согласен и нисколько с этим не спорю. Я только повторяю заезженную истину о том, что дьявол в деталях. NGG>>Как делается настройка "и поведения, и данных"? NGG>>Как повторно использовать "труды" настройки?
BZ>передачей их в качестве параметров BZ>видимо, речь идёт о partial application?
Сарказм понятен, переформулирую вопрос.
Чтобы передать "в качестве параметров" нужно как-то создать эти парметры.
Как они будут создаваться?
Как избежать дублирования кода, если метод дергается из разных мест в коде со слегка различающимися параметрами?
Не дурак, понимаю, что общая логика должна быть вынесена методы.
Тогда почему в предлагаемых решениях они не вынесены?
Опять не дурак, опять понимаю, что на кой их выносить, если всё в одной строчке.
Ну так я же просил показать как будет выглядеть решение "боевой" задачи, как если бы это было решение, которое ждёт большее будущее и долгая история развития
У меня подозрение, что проделай вы всё это, вместо одной строчки было бы что-то похожее по количеству строчек на код VoidEх'a.
Ну раз этот поинт никому не интересен можно забить на мои вопросы.
BZ>4) нужен двухсторонний автовывод типов с автоматическим обобщением. этого тоже нет, что затрудняет разбиение алгоритмов на множество небольших [глобальных, локальных и анонимных] функций — засребёшься описывать типы всех их параметров и результатов
Кстати, да. Декларации типов с генериками могут засрать код хорошо... Даже без относительно к фп хотелось бы такое иметь в наличии
BZ>удобно ли это? на мой взгляд нет — нужно сгруппировать описание контрола, перевод данных в него и обратно. соответственно ФП-дизайн здесь состоит в создании высокоуровневых контролов, которые несут в себе не только свои данные, но и ассоциированные с ними действия. и тогда программа будет выглядеть так: BZ>
Пока гуй простецкий это работает. Пробовал на практике, для java есть возможность так писать используя Groovy и JavaFX. Но как только действия становятся сложнее, чем простое присвоение значения и когда контролы зависят друг от друга многими связями, плюсов станосится как-то заментно поменьше... mvc опять начинает рулить.
Но подход "dsl средствами языка" мне тоже нравится. Жаль, что в синтаксис java досточно убог для его широкого применения. Жду java7, где обещают быть улучшения в этом плане.
Здравствуйте, dmz, Вы писали:
dmz>Слать это в сокет очень полезно, без сомнения. А вот сделаем реалистичное допущение относительно данной задачи в рамках ее предметной области (уж какая есть) — писать в каждый файл только уникальные строки (т.к. на кой черт писать дублирующие?)
dmz>решение на условном ФП: dmz>concrete_filter = unique . filter filt dmz>[/code]
NGG>Ну так если затем расписать все перечисленные в аргуметах интерфейсы классы, получится тоже самое количество кода что в обрисованном мной вараинте.
В джаве — да. За нее я и не говорю.
NGG>>>Если расписать всё это, получатся теже яйца, что и в оо варианте вид сбоку
Что в ФП варианте можно расписать? Приводился — ранее — вполне рабочий код.
NGG>СompositeGroup — сам алгоритм
Спорная вещь — засовывания алгоритма в класс, который выглядит как контейнерный...
NGG>Разве не яйца?
И как-то очень много.
NGG>Наверное, очевидным должно быть, что не только для раз и на всегда заданных условий, а для более широго круга задач NGG>Вот собственно поддтверждения этому я и хочу найти.
Ну какие нужны подтверждения? Когда меняется ситуация, редактировать ФП код как-то проще, поскольку его обычно меньше. Общие вещи, абстракции — выделяются и выносятся. Рассуждать 'в общем' мне тяжело — лучше на конкретных примерах. Вот есть у меня компилятор, который пережил несколько смен синтаксиса и еще переживет. Там нет ничего, что можно было бы унаследовать, и создать, например, новый компилятор путем наследования и подмешивания к существующему. Плохо ли это? Нет, это только хорошо.
NGG>А что он должен поддреживать? Только анонимные функции? Т.е. в C# уже на ура будет работать?
Ну вот, композицию функций, например. Алгебраические типы. Паттерн-матчинг.
dmz>> Ну вот пример — был у нас фильтр непустых строк, но, допустим, надо бы отсортировать комментарии и вообще — уметь наложить вместо одного фильтра N фильтров, но фильтры известны в компайл-тайме. Ну, в общем, вполне себе допущение, не хуже уже прочих.
NGG>Гхм. Это вообще-то называется AndFilter и его добавление не приводит к изменниям в коде. Тот самый OCP в действии
Его написать надо, или его можно сгенерировать из имеющихся фильтров? Или есть конструкция AndFilter<FilterA, FilterB, ...> которая его породит?
NGG>Реализовывать его, пока он не был нужен и было бы овердизайном и заточкой под изменения, которые никогда не случатся (в отличии от декларации интерфейсов).
Ну так impact, который получится при таком изменении требований — он в итоге разный в ФП и ООП?
NGG>Только цель предложенного дизайна была не показать как ооп крут, а фп гавно (такое ощущение, что многими, кто мне отвечал, оно воспринимается именно так). В том сообщении я задал вопрос про то, как выглядело бы решение в функциональном стиле будь это не задачка на две строчки, а "реальная" задача.
Как можно показать решение "не на две строчки" в двухстрочной задаче?
NGG>ОО-код служит примером того, как я ответил бы на этот вопрос задай мне его кто-нибудь. Просто иллюстрация, чтобы было понятно о чём я спрашиваю. NGG>Похоже канал коммуникации этим приёмом настроить не удалось
Ну, наверное, не лучший пример. Не знаю, где взять лучший. Попробую сформулировать свою позже.
NGG>Вот я сейчас в роли такого человека оказался. Перед глазами три "фреймоврка" для "легкого" построения гуя в одном приложение и все настолько безобразны (7+ лет приложению), что я плачу. Уже давно мечтаю всё выкинуть и написать своё, бо цена рефакторинга почти такая же, но если текущая задача стоит Х, переписать и выполнить текущую задачу "красиво" будет стоить 100*X, если не больше. Приходится закрывать глаза и получать удовльствие
Объем задачи другой. Но ведь тут уже ничего не поможет — ни фп, ни ооп.
NGG>Собственно эта ситуация отчасти поспособствовала тому, что я стал задавать тут глупые вопросы. Стало интересно, что было бы, пиши это приложение в течении 7 лет теже недалёкие люди, но только на функциональном языке. Больше гавна пришлось бы разбирать или меньше?
Все также зависит от человеческого фактора. ФП — очень мощный инструмент. В правильных руках — он послужит сокращению сроков и издержек. Но не в тех руках — оно будет только во вред. Преимущество ФП в том, что в нем есть встроенная защита от дурака. Недостаток ФП в том, что дурак, обошедший эту защиту — стоит сотни обычных по разрушительным последствиям.
Инструмент нужно выбирать по задаче. Для меня в моих условиях — ФП является более выгодным инструментом. Для кого-то — вероятно, нет.
Как мне кажется — ФЯ являются более мощными и более высокоуровневыми, по сравнению с распространенными обычными языками.
dmz>>решение на условном ФП: dmz>>concrete_filter = unique . filter filt dmz>>[/code]
NGG>Тоже самое на условной java:
Не говоря о том, что здесь всего больше, мне как-то неочевидно, как это должно использоваться.
Re[5]: Мастер-класс по ФП
От:
Аноним
Дата:
04.01.09 21:00
Оценка:
Здравствуйте, dmz, Вы писали:
dmz>Не говоря о том, что здесь всего больше, мне как-то неочевидно, как это должно использоваться.
вместо new FilterImp(...) ставится new UniqueFilter(new FilterImpl()) и используется. FilterImpl это реализация фильтра, который нужно сделать уникальным...
А>вместо new FilterImp(...) ставится new UniqueFilter(new FilterImpl()) и используется. FilterImpl это реализация фильтра, который нужно сделать уникальным...
Предложенный интерфейс фильтра подразумевает, что он применяется к одному элементу:
interface IFilter<T>
{
boolean match(T t);
}
и в реализации так и происходит. Уникальные элементы таким образом не отфильтруешь.
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>Использование функции c не безопасно — н-р, можно при правке кода поменять a на a', а про b забыть... NGG>Компилятор промолчит. Может быть даже тесты пройдут успешно, т.к. разница между b и b' крылась в каком-то редком случае почему-то не покрытому при тестировании. А в итоге приложение не отработает как следует в самый ответственный момент.
Ну и что тут необычного? Это постоянно происходит в любых языках и парадигмах -- ФП, очевидно, не является серебрянной пулей.
Конкретно с этой проблемой может быть могут немного уменьшить опасность зависимые типы, когда их сделают простыми для использования. И то не на 100%...
NGG>Возможно моя боязнь блуждающих по аргументам в свободном полёте связных функций это только от недостатка опыта в их готовке... NGG>Но пока мне кажется, что разумно было бы явно декларировать зависимость между аргументами: NGG>
NGG>Из декларации сразу видно что к чему. NGG>Разве нет?
Ну Вам же никто не запрещает так делать? Нужно -- так делайте! Все возможности для этого у Вас есть...
NGG>Гхм. Тут какое-то расхождение во взглядах на то, что такое монолитность NGG>Мне как раз твоё решение видится монолитным кусоком кода, а моё комбинируется из нескольких классов для полученя нужного результата и каждый из этих классов может жить счастливой самостоятельной жизнью.
Делайте решение в ФП-стиле -- то есть кучу маленьких функций, максимально обобщённых. И тогда останется просто комбинировать их в нужном порядке -- вот и не будет этой страшной монолитности...
Здравствуйте, BulatZiganshin, Вы писали: BZ>как-то всё это сложно
Ну я это и нарисовал. Только использовал списковые дополнения вместо map-ов
Вместо mapMaybe (stripPrefix (make dir)) выписал списковое доплнение через take и drop для случая непустого имени (с монадами пока торможу). mapSnd — тоже выписал явно.
По ходу есть ещё одно препятствие в изучении ФП, но оно довольно общее для любых языков — пока не очень свободно знаешь язык и его стандартные библиотеки, очень трудно распознать алгоритм в чужом коде.
И тем более предсказать трудоёмкость.
Например, решение через take и drop скорее всего проигрывает по сравнению с mapMaybe (stripPrefix (make dir)). По крайней мере в С/С++ я мог бы это отразить. Но как это в haskell-е я пока не в курсе.
Здравствуйте, BulatZiganshin, Вы писали:
BZ>вот пример. представим классическую gui-библиотеку. реализация диалога настроек на ней может выглядеть так: BZ>1) создаём все необходимые controls BZ>2) заполняем их исходными данными BZ>3) отображаем диалог BZ>4) после нажатия Ok обновляем переменные
BZ>удобно ли это? на мой взгляд нет — нужно сгруппировать описание контрола, перевод данных в него и обратно. соответственно ФП-дизайн здесь состоит в создании высокоуровневых контролов, которые несут в себе не только свои данные, но и ассоциированные с ними действия. и тогда программа будет выглядеть так: BZ>
Кстати, вот животрепещущая тема (для меня), можете посоветовать что-нить в этом плане? Имею в виду сами принципы построения такого фреймворка применительно к Хаскеллу. Всякие там Model-View-Controller в функциональном стиле...
Здравствуйте, Tonal-, Вы писали:
T>По ходу есть ещё одно препятствие в изучении ФП, но оно довольно общее для любых языков — пока не очень свободно знаешь язык и его стандартные библиотеки, очень трудно распознать алгоритм в чужом коде.
С ФП с этим полегче по моему. Вот при изучении Smalltalk у человека знающего майнстримный ООП приключается когнитивный диссонанс
Для того, чтобы код был аналогичен, нужно однако чуть добавить:
сlass TaskNumberOne {
template <class Source, class Filter, class Group, class Consumer>
void process(Source inp, Filter filt, Group group_by, Consumer consume) {...}
Тогда мы можем воспользоваться "трудами" фильтра в группировке, только они должны имеь согласованный интерфейс.
При этом возникает ещё 2 типа — то что получилось после фильтрации и то, что получилось после группировки. Но если попытаться их здесь явно выписать, то получаются уже шаблоны шаблонов и сигнатура получается монстрообразная, а на Java или C# просто невыразимая...
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>Ну так я же просил показать как будет выглядеть решение "боевой" задачи, как если бы это было решение, которое ждёт большее будущее и долгая история развития NGG>У меня подозрение, что проделай вы всё это, вместо одной строчки было бы что-то похожее по количеству строчек на код VoidEх'a.
В таких проектах объём документации должен быть на порядок больше объёма кода, иначе проблемы будут всегда. И комментарии в коде должны быть обширными. И вапще Literate Programming тут уже напрашивается.
В итоге подходим к тому, что программа должны быть исполняемым руководством...
Здравствуйте, geniepro, Вы писали:
G>Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>>Использование функции c не безопасно — н-р, можно при правке кода поменять a на a', а про b забыть... NGG>>Компилятор промолчит. Может быть даже тесты пройдут успешно, т.к. разница между b и b' крылась в каком-то редком случае почему-то не покрытому при тестировании. А в итоге приложение не отработает как следует в самый ответственный момент.
G>Ну и что тут необычного? Это постоянно происходит в любых языках и парадигмах -- ФП, очевидно, не является серебрянной пулей. G>Конкретно с этой проблемой может быть могут немного уменьшить опасность зависимые типы, когда их сделают простыми для использования. И то не на 100%...
Почему не на 100%? Если использовать технику наподобие доступной в COQ, когда одновременно генерируется программа на Хаскеле или Камле и доказательство корректности всех ее требуемых инвариантов, то таки на 100% Правда назвать использование COQ простым язык не поворачивается
Здравствуйте, deniok, Вы писали:
D>Почему не на 100%? Если использовать технику наподобие доступной в COQ, когда одновременно генерируется программа на Хаскеле или Камле и доказательство корректности всех ее требуемых инвариантов, то таки на 100% Правда назвать использование COQ простым язык не поворачивается
Так вот тут-то и проблема -- указать все инварианты просто нереально, всегда появятся новые требования, и где-то что-то будет забыто. А значит и доказательство соответствия инвариантам не будут 100%-полными...
G>Так вот тут-то и проблема -- указать все инварианты просто нереально, всегда появятся новые требования, и где-то что-то будет забыто. А значит и доказательство соответствия инвариантам не будут 100%-полными...
Не, ну если ты забыл запрограммировать новые требования или откомпилировать их, то да. Но если тебе удалось откомпилировать программу на COQ, то результирующий хаскелловский код удовлетворяет всем инвариантам, при этом формальное доказательство этого факта лежит рядом.
Re[7]: Мастер-класс по ФП
От:
Аноним
Дата:
05.01.09 11:03
Оценка:
Здравствуйте, dmz, Вы писали:
dmz>Уникальные элементы таким образом не отфильтруешь.
dmz>>Уникальные элементы таким образом не отфильтруешь.
А>Упражнение для первого класса...
Да, как-то уже варианты с побочными эффектами фильтруешь бессознательно. Минуса два — первый —
накопление элементов в фитре (по сути — предикате), в там, где мы совсем не ожидаем.
Второй — если нам потребуется операция, которая обрабатывает всю коллекцию — мы можем или дальше
хачить фильтр — т.е. в первый проход накапливаем, во второй фильтруем — или вводить новую сущность — собственно,
сам процесс фильтрации, интерфейс, класс и вставлять его в цепочку операций. Делать в фильтре — фейл дизайна,
вводить новую сущность — куча писанины для единичной элементарной операции.
Здравствуйте, Tonal-, Вы писали:
BZ>>как-то всё это сложно T>Ну я это и нарисовал. Только использовал списковые дополнения вместо map-ов
в чём для меня существенная разница. ты это рассматриваешь в том стиле, который я считаю императивным: у нас есть переменные, мы выполняем над ними какие-то операции и получаем результат. я действую в стиле — у нас есть входное значение и выходное, мы описываем преобразование из одного в другое, и именно это я считаю ag-подходом к описанию алгоримтов: любая решаемая задача сводится к преобразованию данных, которое мы описываем через декомпозицию вплоть до элементарных операций
T>И тем более предсказать трудоёмкость. T>Например, решение через take и drop скорее всего проигрывает по сравнению с mapMaybe (stripPrefix (make dir)). По крайней мере в С/С++ я мог бы это отразить. Но как это в haskell-е я пока не в курсе.
меньше всего интересует. в большинстве случаев неважно, в остальных это только один из аспектов оптимизации
NGG>>Компилятор промолчит. Может быть даже тесты пройдут успешно, т.к. разница между b и b' крылась в каком-то редком случае почему-то не покрытому при тестировании. А в итоге приложение не отработает как следует в самый ответственный момент.
G>Ну и что тут необычного? Это постоянно происходит в любых языках и парадигмах -- ФП, очевидно, не является серебрянной пулей.
"Серебрянная пуля" имеет вполне определенный смысл — это уменьшение труда программиста.
Здравствуйте, NotGonnaGetUs, Вы писали:
BZ>> есть список слов. надо записать в файл "a" все слова, начинающиеся на букву 'a', в файл "b" — слова на 'b' и т.д. вплоть до z
NGG>После чего глубоко задумался.
NGG>Прежде всего обобщил бы постановку задачи:
NGG>Дано: источник данных, критерий разбиения данных на группы NGG>Надо: данные разбить на группы, обработать данные в группах.
NGG>Может кто-нибудь из "гуру" набросать, как будет выглядеть "обобщённое" решение 1-ой задачки в функциональном стиле?
Вернулся к обсуждению имевшему место быть какое-то время назад на этом форуме и нашёл слова thesz'a о том, что в цепочке
дизайн -> реализация -> отладка использование фя позволяет упростить две последнии стадии, а с первой могут помочь формальные спецификации.
Посмотрел на нашу битву реализаций и решил-таки (благо новый год и есть свободное время) познакомиться с одним из языков формальных спецификаций (Z) поближе, что в итоге позволило взглянуть на поднятую тему под немного другим углом.
Прежде всего, предложенное обобщение слишком "обобщенное": для того, чтобы перейти к дизайну, а затем реализации, требуется принять ряд решений (наложить ограничения) от которых итоговое решение довольно сильно зависит. Эти ограничения не имеют никакой связи с языком программирования или "стилем" проектирования принятым в этом языке.
Вернёмся к задаче в исходной постановке.
Её можно разбить на подзадачи разными способами.
Н-р:
1)
— Получить из списка строк Х список строк, в котором все слова начинаются с буквы Y (filterByFirstLetter :: [String] -> Char -> [String]).
— Записать в файл с именем X список слов Y (write :: String -> [String] -> IO())
Тогда итоговое решение выглядит так:
mapM_ (\letter -> write [letter] (filterByFirstLetter theData letter)) ['a'..'z']
2)
— Записать в файл с именем Х все слова из списка Y начинающиеся с префикса Х (writeWithPrefix :: String -> [String] -> IO())
Тогда итоговое решение выглядит так:
mapM_ (\letter -> writeWithPrefix [letter] theData) ['a'..'z']
3)
— Реализовать (или использовать существующую) инфраструктуру mailbox'ов.
— Реализовать компонент, который получая из mailbox'a слово пишет его в файл с именем Х.
— Реализовать компонент, который получая из mailbox'a слова пересылает их по первой букве в другие mailbox'ы используя маппинг (буква -> mailbox)
Тогда итоговое решение выглядит так (мифический синтаксис):
mapping = map (\letter -> (letter, makeFileMailBox [letter])) ['a' .. 'z']
dispatcher = makeDispatcherMailBox mapping
mapM_ (\word -> dispatch dispatcher word) theData
И т.п.
Т.е. на лицо очевидный факт, что одну и туже задачу можно решить разными способами.
Коль скоро это так, возникает закономерный вопрос: а какое из решений лучше?
То что короче или то что понятнее или то что создаёт больше повтроноиспользуемых артефактов или то что производительнее или то что использует больше существующих артефактов (и потому быстрее реализуется) или или или ...
Вот тут основная загвоздка и находится, т.к.
— Чтобы ответить на этот вопрос нужно знать контекст, в котором происходит оценка (является ли данная задача самодостаточной или частью общей задачи, какова вероятность внесения изменений в постановку задачи и какого рода будут эти изменения и т.д. и т.п.)
— Часть критериев качества определяется ой-как не формально и потому существует больше пространство для жарких споров
Раз нет общепринятых формальных критериев, появляются эвристики выражаемые в той или иной форме.
Н-р, в ОО-мире — это разнообразные шаблоны (gof++) — стандартные решения с описанием их плюсов и минусов, антишаблоны — примеры часто встречающихся плохих решений ,"запахи" — характеристики кода, который потенциально (но не обзяательно) приведёт к проблемам в будущем (трудность поддрежки), всякие разные принципы — LSP, OCP, закон деметры и т.п.
(Оффтоп: эти эвристики иногда принимают за истину в поледней инстанции, убеждаются, что это не так и начинают хаять по чём зря, вместо того, чтобы разобраться когда и где они работают (Имхо, из всего зоопарка нужно знать только шаблоны grasp, т.к. они, как аксиомы в геометрии, дают базис на основе которого можно вывести практически любую известную "эвристику" и получить представление о том, когда ей стоит следовать, а когда нет)).
Т.к. необдуманное следование эвристикам ведёт к противоположному от ожидаемого результату и т.к. переделка всегда сопряжена с рисками (наплодить новых багов + потратить кучу времени без осязаемого полезного выхода) популярностью пользуются подходы:
"раз работает — не тронь" и "использовать первое пришедшее в голову решение" (или его более интеллектуальные вариант — "everything should be made as simple as possible, but no simpler."). При этом разрешение рисков откладывается на будущее, на момент когда они "стрельнут". И практика показывает, что такой подход к разработке, хоть и не без греха, но имеет право на жизнь.
Причём, насколько мне позволяет судить опыт (и обрывки информации почерпнутые, в том числе, на форумах rdsn) результативность следования "заветам простоты" при программировании с использованием ФЯ выше, чем если используется какой-либо из императивных языков. Причин вижу две:
— актвиное использование неизменяемых структур данных минимизирует появление сайд эффектов и, как следствие, приводит к решениям с меньшей связностью (значит вносить изменения и тестировать код существенно проще).
— лаконичный синтаксис + декларативный стиль описания алгоритмов позволяет писать достаточно крупные фрагменты кода не заботясь о его будщей судьбе, т.к. цена внесения в него измнений и переписывания "с нуля" становится сопоставимой.
(Как будет работать ФП при увеличении сложности системы — не знаю.
Время потраченное на дизайн, наверняка, будет сопоставимым со временем потраченным на дизайн в ОО-стиле.
Время потраченное на реализанию меньше, чем в случае "многословных" языков без вывода типов и синтаксического сахара, но не думаю, что на порядок (у "многословных" языков есть чудо-IDE в рукаве (с автоматическими рефакторингами, отладчиками, профайлерами и т.п.) и обширная база "готового кода" — библиотек и фреймворков, a у фя больше плясок с достижением желаемой производительности).
На внесение изменений — хз, зависит от того, насколько был хорош тот и другой дизайны.)
Но что-то я слегка отклонился от главной темы. Возвращаюсь.
Да, формально оценить качество решения без контекста невозможно. А субъективные оценки интересны обычно только самим субъектам
Исходя из этого моё исходное желание увидеть "промышленный" функциональный подход в действии на примере задачки про файлы было обречено не сбыться, т.к. сила фп как раз в том, чтобы быть всегда kiss, т.е. не обобщать без нужды
И тем не менее! Буду отвечать себе сам
Допустим я при реализации выбрал решение 1) т.к. решение 1 в 1 совпадает с постановкой задачи.
Потом мне говорят — надо ещё уметь по первым трём буквам строить файлы.
Я, следуя "заветам" пишу:
theData = ["abc", "def", "ijk", "axxx", "der", "preved", "%%%%%", "12345"]
type Discriminator a b = (a -> b)
type Group a b = (a, [b])
type GroupAction a b c = (Group a b -> c)
groupByF :: Ord b => Discriminator a b -> [a] -> [Group b a]
groupByF discriminator list =
list >>== sortByF discriminator >>== groupByF discriminator >>== map (\list -> (discriminator $ head list, list))
filterByD :: (a -> Bool) -> [Group a b] -> [Group a b]
filterByD f = filter (\(groupId,_) -> f groupId)
writeConsole :: GroupAction String String (IO())
writeConsole (groupId, []) = return ()
writeConsole (groupId, listOfWords) = putStrLn $ groupId ++ " -> " ++ (show listOfWords)
(>>==) :: a -> (a -> b) -> b
(>>==) x f = f x
sortByF f = sortBy (\x y -> (f x) `compare` (f y))
groupByF f = groupBy (\x y -> (f x) == (f y))
main = theData
>>== groupByF (take 3)
>>== filterByD ((`elem` [x| x <- ['a'..'z']]).head)
>>== mapM_ writeConsole
Ну, переписал всё и ладно.
А тут просят: а теперь решено формировать файлы не по префиксу, а по "содержит".
Опа... опять не получается простым движением руки изменение внести, т.к. функция дикриминатор превратилась в список дикриминаторов, причём с другой сигнатурой.
Опять переписывать groupByF?
А если в следующий раз попросят от 'a' до 'n' по префиксу писать, а от 'o' до 'z' по "содержит"?
Ешкин кот — ни старый groupByPrefix, ни новый groupByContains уже не используешь — опять писать что-то нужно.
Ну напишем:
theData = ["abc", "def", "ijk", "axxx", "der", "preved", "%%%%%", "12345", "zoo"]
type Group = (String, [String])
type GroupAction a = (Group -> a)
data Discriminator = DByPrefix String | DByContains String | DByPostfix String
match text (DByPrefix prefix) = isPrefixOf prefix text
match text (DByContains prefix) = isInfixOf prefix text
match text (DByPostfix prefix) = isSuffixOf prefix text
groupId (DByPrefix prefix) = prefix
groupId (DByContains prefix) = prefix
groupId (DByPostfix prefix) = prefix
groupByD :: [Discriminator] -> [String] -> [Group]
groupByD ds ws = (groupByD' ds ws)
>>== sortByF fst >>== groupByF fst
>>== map (\x -> (fst $ head x, map snd x))
where
groupByD' ds [] = []
groupByD' ds (w:ws) = case find (match w) ds of
Just x -> (groupId x, w) : (groupByD' ds ws)
Nothing ->groupByD' ds ws
filterByD :: (String -> Bool) -> [Group] -> [Group]
filterByD f = filter (\(groupId,_) -> f groupId)
writeConsole :: GroupAction (IO())
writeConsole (groupId, []) = return ()
writeConsole (groupId, listOfWords) = putStrLn $ groupId ++ " -> " ++ (show listOfWords)
(>>==) :: a -> (a -> b) -> b
(>>==) x f = f x
sortByF f = sortBy (\x y -> (f x) `compare` (f y))
groupByF f = groupBy (\x y -> (f x) == (f y))
main = theData
>>== groupByD ((map DByPrefix [[x]| x <- ['a'..'n']]) ++ (map DByContains [[x]| x <- ['o'..'z']]))
>>== filterByD ((`elem` [x| x <- ['a'..'z']]).head)
>>== mapM_ writeConsole
А тут говорят, хочу слова начинающиеся в префикса 'a' писать в консоль, а содрежащие 'a' или начинающиеся с 'b' в файл и имя файла сам хочу выбрать...
Скрепя сердцем делаю:
theData = ["abc", "def", "ijk", "axxx", "der", "preved", "%%%%%", "12345", "zoo"]
--- IFilter and implementations?data Discriminator = DByPrefix String | DByContains String | DByPostfix String
matchD text (DByPrefix prefix) = isPrefixOf prefix text
matchD text (DByContains prefix) = isInfixOf prefix text
matchD text (DByPostfix prefix) = isSuffixOf prefix text
discrId (DByPrefix prefix) = prefix
discrId (DByContains prefix) = prefix
discrId (DByPostfix prefix) = prefix
--- IProcessor and implementations ?type GroupAction = (Group -> IO())
makeConsoleAction1 :: String -> GroupAction
makeConsoleAction1 _ (_, []) = return ()
makeConsoleAction1 name (_, listOfWords) = putStrLn $ name ++ " (prefix)-> " ++ (show listOfWords)
makeConsoleAction2 :: String -> GroupAction
makeConsoleAction2 _ (_, []) = return ()
makeConsoleAction2 name (_, listOfWords) = putStrLn $ name ++ " (contains)-> " ++ (show listOfWords)
writeConsole :: GroupAction
writeConsole (_, []) = return ()
writeConsole (groupInfo, listOfWords) = putStrLn $ (groupId groupInfo) ++ " -> " ++ (show listOfWords)
--- IGroup and implementations ?data GroupInfo = FilteredGroup GroupAction Discriminator
discriminator (FilteredGroup _ d) = d
action (FilteredGroup a _) = a
match text groupInfo = matchD text $ discriminator groupInfo
groupId groupInfo = discrId $ discriminator groupInfo
--- mutable (in oo) state of IGroup or IProcessor ... type Group = (GroupInfo, [String])
processGroup (groupInfo, listOfWords) = (action groupInfo) (groupInfo, listOfWords)
--- group builder...
groupByD :: [GroupInfo] -> [String] -> [Group]
groupByD ds ws = (groupByD' ds ws)
>>== sortByF (groupId.fst) >>== groupByF (groupId.fst)
>>== map (\x -> (fst $ head x, map snd x))
where
groupByD' ds [] = []
groupByD' ds (w:ws) = case find (match w) ds of
Just info -> (info, w) : (groupByD' ds ws)
Nothing -> groupByD' ds ws
filterByD :: (String -> Bool) -> [Group] -> [Group]
filterByD f = filter (\(groupInfo,_) -> f $ groupId groupInfo)
(>>==) :: a -> (a -> b) -> b
(>>==) x f = f x
sortByF f = sortBy (\x y -> (f x) `compare` (f y))
groupByF f = groupBy (\x y -> (f x) == (f y))
--- solution itself
main = theData
>>== groupByD groupInfos
>>== filterByD ((`elem` [x| x <- ['a'..'z']]).head)
>>== mapM_ processGroup
where
groupInfos = (map (\d -> FilteredGroup (makeConsoleAction1 (discrId d)) d) (map DByPrefix [[x]| x <- ['a'..'n']]))
++ (map (\d -> FilteredGroup (makeConsoleAction2 (discrId d)) d) (map DByContains [[x]| x <- ['g'..'z']]))
... и видим как близко решение оказывается к глупому оо.
А когда замечаем, что между делом потерялся способ сгруппировать по первым нескольким буквам ... становится ясно, что ещё копать и копать до финала. Слёта даже не могу придумать как это более или менее красиво реализовать четвёртый тип дискриминатора (DByLength Int)
За сим откланиваюсь, будет критика пишите.
з.ы.
К дискуссии о "промышленном" решении можно будет вернуться, когда ФП окажется в массах и массы, пробежавшись по всем граблям n-раз, обобщат их до шаблонов, "запахов" и приниципов
Здравствуйте, BulatZiganshin, Вы писали:
BZ>я часто сталкиваюсь с двумя точками зрения — первая "я знаком с ФП, но оно никакой выгоды в программировании не даёт", и вторая "я знаком с ФП, но так и не научился использовать его для повышения эффективности программирования". собственно, это одно и то же, разница только в том, на кого (ФП или самого человека) возлагается вина за отсутствие результата
Еще одно мнение в копилку — ФП дает выигрыш на узком классе задач, связанных со сложной работой с динамическими структурами данных (списками, деревьями, etc) — на что оно, собственно, и заточено. А на том, что типично пишут программисты, оно особенно не нужно.
Кстати, давно хочу полюбоваться, как на Хаскелле запишется следующий императивный код:
void DoIt() {
A++;
B[C].D*=10;
F(G).H=0;
}
Хаскеллисты что-то отказываются решать такую задачу.
Серьезно хочу, не только чтобы какие-то там языки унизить — свои-то минусы можно найти у любого языка.
And if you listen very hard the alg will come to you at last.
Здравствуйте, deniok, Вы писали:
D>Это не задача. Это выглядящие бессмысленными упражнения с записыванием значений в непонятные участки памяти, очевидно, доступные глобально.
Я считаю, что начинать надо с малого. Лично мне не очень понятно, как добиться на Хаскелле решения такой простой проблемы и (серьезно!) хочу его увидеть.
Оно не такое уж и бессмысленное. У меня при программировании часто возникает код, подобный написанному. Хочу понять, как получить его аналог в функциональных языках.
Некоторые тут, кстати, заявляют, что Хаскель лучший императивный язык...
And if you listen very hard the alg will come to you at last.
Здравствуйте, subdmitry, Вы писали:
S>Здравствуйте, deniok, Вы писали:
D>>Это не задача. Это выглядящие бессмысленными упражнения с записыванием значений в непонятные участки памяти, очевидно, доступные глобально.
S>Я считаю, что начинать надо с малого. Лично мне не очень понятно, как добиться на Хаскелле решения такой простой проблемы и (серьезно!) хочу его увидеть.
S>Оно не такое уж и бессмысленное. У меня при программировании часто возникает код, подобный написанному. Хочу понять, как получить его аналог в функциональных языках.
Если у тебя возникает такой код, то это очень плохо даже в императивных языках. У тебя там семь идентификаторов, смысл которых неизвестен в рамках данной функции. Такая связность по данным DoIt с внешним миром бессмысленна. Даже в обычном процедурном программировании считается предпочтительным, чтобы внешний мир общаелся с функцией через ее аргументы, а не через глобальные данные.
Здравствуйте, deniok, Вы писали:
S>>Оно не такое уж и бессмысленное. У меня при программировании часто возникает код, подобный написанному. Хочу понять, как получить его аналог в функциональных языках.
D>Если у тебя возникает такой код, то это очень плохо даже в императивных языках. У тебя там семь идентификаторов, смысл которых неизвестен в рамках данной функции. Такая связность по данным DoIt с внешним миром бессмысленна.
Да нормальная связанность. Предположим, что у нас есть функция на 100 строк, три из которых что-то меняют в глобальном состоянии программы. Предположим, что я привел только эти три строчки и хочу узнать, как их реализовать.
Если тебе нужен пример из реальной жизни, представь себе download manager (качалку файлов), который при получении очередной порции данных по сети изменяет счетчик всех скачаных байт за сеанс, изменяет количество скачанных байт у данного файла и изменяет количество скачанных байт у данного типа (группы) файлов, который определяется для данного файла посредством вызова функии. Вот тебе и три воздействия на глобальные переменные в одной функции (уж не будем зацикливаться на деталях, что там за операции именно используются).
Для полного прикола можно еще представить, что такие функции вызываются в каждом независимо работающем треде, обслуживающем скачку одного из нескольких одновременно качающихся файлов, так что передавать состояние системы в функцию и получать его назад измененным не получится.
D>Даже в обычном процедурном программировании считается предпочтительным, чтобы внешний мир общаелся с функцией через ее аргументы, а не через глобальные данные.
Не разделяю этих религиозных воззрений. Даже люди, разрабатывающие функциональный подход, признают, что у программы может быть состояние. Ну а доступ к состоянию удобно реализовывать через глобальные переменные — клинический факт. Просто так получается намного короче.
Короче надо понимать ситацию так, что реализация такой вещи на Хаскелле настолько страшна, что ее никто не приведет.
And if you listen very hard the alg will come to you at last.
Здравствуйте, NotGonnaGetUs, Вы писали:
BZ>>> есть список слов. надо записать в файл "a" все слова, начинающиеся на букву 'a', в файл "b" — слова на 'b' и т.д. вплоть до z NGG>>Прежде всего обобщил бы постановку задачи: NGG>>Дано: источник данных, критерий разбиения данных на группы NGG>>Надо: данные разбить на группы, обработать данные в группах. NGG>> как будет выглядеть "обобщённое" решение 1-ой задачки в функциональном стиле?
import Data.List
import Data.Ord
-- РЕШЕНИЕ
-- универсальная функция, группирует список пар (ключ, значение) по ключу, возвращая список пар (ключ, [значения для этого ключа])
-- например groupByFst [(1,2), (1,3), (2,5), (1,4), (2,6)] возвращает [ (1,[2,3,4]), (2,[5,6]) ]
groupByFst ps = [ (fst $ head x, map snd x) | x <- groupBy (\a b -> fst a == fst b) $ sortBy (comparing fst) ps]
-- группируем слова по месту назначения на основе fun (слово -> список куда записать), получаем список пар (место назначения, слова)
-- затем вызываем обработчик process (куда записать -> слова -> IO) для каждой такой пары
processWords fun process words = mapM_ write $ groupedWords
where
write (target, words) = process target words
groupedWords = groupByFst $ [ (target, word) | word <- words, target <- fun word]
-- ИСПОЛЬЗОВАНИЕ
data Target = Console | File String | TempFile deriving (Eq, Ord, Show)
process :: Target -> [String] -> IO ()
process (File filename) words = do
putStrLn $ "To file " ++ filename ++ " : "
mapM_ putStrLn words
process Console words = do
putStrLn $ "To console: "
mapM_ putStrLn words
process TempFile words = do
putStrLn $ "To temp file: "
mapM_ putStrLn words
-- записываем слово в файл в соответствие с первой буквой слова
targets word = [File $ take 1 word ++ ".txt", Console]
-- записываем слово в файл в соответствие с тремя первыми буквами слова
targets2 word = [File $ take 3 word ++ ".txt", Console]
-- записываем слово в файл "?.txt" если слово содержит букву ?
-- дополнительно слова, длиной 3 пишем во временный файл
-- дополнительно слова, начинающиеся с гласной буквы пишем в консоль
targets3 word = byContain word ++ byLen3 word ++ byVowel word
where
byContain letters = [File $ s:".txt" | s <- letters]
byLen3 letters | length letters == 3 = [TempFile]
| otherwise = []
byVowel letters | head letters `elem` ['e','u','i','o','a'] = [Console]
| otherwise = []
-- MAIN
sourceData = ["test1", "test2", "aaa", "aaz", "aabb", "happy", "new", "year"]
main = do
processWords targets3 process sourceData
Непосредственно обработкой занимается processWords (универсальное решение)
Обработку мы настраиваем двумя функциями (передаются в качестве параметра processWord):
1. targets — по имени файла говорит, куда этот файла надо записать (в консоль, в конкретный файл, никуда)
2. process — выполняет саму обработку группы слов
На самом деле, то же можно повторить и в ООП Разный подход будет в "настройке".
Думаю решение на ООП будет выглядеть чуть по сложнее, т.к., на мой взгляд, функцию легче охватить взглядом и понять, чем класс ООП.
Не говоря уже о добавлении нескольких классов / интерфейсов.
NGG>Исходя из этого моё исходное желание увидеть "промышленный" функциональный подход в действии на примере задачки про файлы было обречено не сбыться, т.к. сила фп как раз в том, чтобы быть всегда kiss, т.е. не обобщать без нужды
В любом случае "промышленный" код будет потяжелее.
Скормил программке файл на 130 мегов, подавилась, пришлось переписать без сортировки, с чтением файлов по-строчно, хранением хэндлов и т.п.
А из отличий ФП и ООП я бы выделил то, что в ФП легче искать и выделять абстракции. А также легче читать (и писать) программу, т.к. работаешь с меньшим количество кода. И не потому, что ФП лаконичнее, а скорее потому, что независимые куски кода получаются значительно короче.
S>Хаскеллисты что-то отказываются решать такую задачу.
Оно и понятно, за такое тебе и грамотные императивщики по башке надают.
S>Серьезно хочу, не только чтобы какие-то там языки унизить — свои-то минусы можно найти у любого языка.
Пойми я тоже не хочу никого унизить, просто тут уже сразу видно невооруженным взглядом, отсутствие какой-либо базы, а без базы, тебе никто не сможет даже объяснить, почему ты не прав.
Lisp is not dead. It’s just the URL that has changed: http://clojure.org
Здравствуйте, subdmitry, Вы писали:
S>Я считаю, что начинать надо с малого. Лично мне не очень понятно, как добиться на Хаскелле решения такой простой проблемы и (серьезно!) хочу его увидеть.
Я бы советовал начинать сразу с throw или longjmp
S>Оно не такое уж и бессмысленное. У меня при программировании часто возникает код, подобный написанному. Хочу понять, как получить его аналог в функциональных языках.
Это плохо, что возникает код, который лезет в какие-то глобальные данные
Здравствуйте, yumi, Вы писали:
Y>Пойми я тоже не хочу никого унизить, просто тут уже сразу видно невооруженным взглядом, отсутствие какой-либо базы, а без базы, тебе никто не сможет даже объяснить, почему ты не прав.
А при чем тут вообще прав/неправ? Был приведен пример реальной задачи (download manager), в которой возникает код, аналогичный привденному. Я не спрашиваю, прав я или неправ, я предлагаю привести решение этой задачи на Хаскелле. Особенно интересно в многопоточном варианте.
Если, конечно, наличествующая у вас база это позволяет сделать, а не просто утверждать, что "глобальные переменные это плохо".
And if you listen very hard the alg will come to you at last.
Здравствуйте, subdmitry, Вы писали:
S>Если тебе нужен пример из реальной жизни, представь себе download manager (качалку файлов), который при получении очередной порции данных по сети изменяет счетчик всех скачаных байт за сеанс, изменяет количество скачанных байт у данного файла и изменяет количество скачанных байт у данного типа (группы) файлов, который определяется для данного файла посредством вызова функии. Вот тебе и три воздействия на глобальные переменные в одной функции (уж не будем зацикливаться на деталях, что там за операции именно используются).
Зачем этим должна заниматься функция получения данных? А если фильтр изменится (на группы файлов)? Это глупо само по себе. А так, функция принимает данные, возвращает кол-во прочитанных байт, которые прибавляются к текущим прочитанным вообще, для данного файла и для группы. Можешь их потом на экран вывести, если хочешь.
S>Для полного прикола можно еще представить, что такие функции вызываются в каждом независимо работающем треде, обслуживающем скачку одного из нескольких одновременно качающихся файлов, так что передавать состояние системы в функцию и получать его назад измененным не получится.
Вот вам полный прикол:
-- Сюда будем писать, сколько байт прочитано
type ByteReads = Chan (Int -> Int)
-- Кушает пользовательский ввод. На 1 - пишет "прочитано 10 байт", на 0 - выход
writer :: ByteReads -> IO ()
writer chan = loop
where
loop = getChar >>= command
command '0' = printf "done\n"
command '1' = writeChan chan (+10) >> loop
command '\n' = loop -- ignore
command c = printf "Illegal: %c\n" c >> loop
-- 1000 раз непрерывно пишет "прочитано cnt байт"
randomWriter chan cnt = do
mapM_ (writeChan chan) $ take 1000 $ repeat (+cnt)
-- Выводит статистику, сколько байт прочитано
reader :: Int -> [Int -> Int] -> IO ()
reader s xs = mapM_ (printf "Received %d bytes\n") $ scanl (\v op -> op v) s xs
main :: IO ()
main = do
chan <- newChan :: IO ByteReads
s <- getChanContents chan
w1 <- forkIO $ reader 0 s -- Читатель
w2 <- forkIO $ randomWriter chan 1 -- Пишет 1000 раз по 1 байту
w3 <- forkIO $ randomWriter chan 2 -- по 2
w4 <- forkIO $ randomWriter chan 5 -- по 5
writer chan -- Команды от пользователя
Кстати, я думал, что в Chan может быть один писатель, но не падает. Везёт, или писателей таки может быть куча?
S>Короче надо понимать ситацию так, что реализация такой вещи на Хаскелле настолько страшна, что ее никто не приведет.
Да нет, вон она как проста, когда надо не изменить 5 глобальных переменных в массивах, а решить саму задачу.
Здравствуйте, subdmitry, Вы писали:
S>Здравствуйте, VoidEx, Вы писали:
VE>>Это плохо, что возникает код, который лезет в какие-то глобальные данные
S>О! Вот и продемонстрируйте, как можно на функциональном языке прекрасно без этого обойтись. Пример в студии.
BZ>>я часто сталкиваюсь с двумя точками зрения — первая "я знаком с ФП, но оно никакой выгоды в программировании не даёт", и вторая "я знаком с ФП, но так и не научился использовать его для повышения эффективности программирования". собственно, это одно и то же, разница только в том, на кого (ФП или самого человека) возлагается вина за отсутствие результата
S>Еще одно мнение в копилку — ФП дает выигрыш на узком классе задач, связанных со сложной работой с динамическими структурами данных (списками, деревьями, etc) — на что оно, собственно, и заточено.
Прошу прощения, но ФП ни под что конкретно не заточено.
Это всё в глазах наблюдателя.
S>А на том, что типично пишут программисты, оно особенно не нужно.
Какие программисты-то?
Yours truly, Serguey Zefirov (thesz NA mail TOCHKA ru)
Здравствуйте, subdmitry, Вы писали:
S>Здравствуйте, deniok, Вы писали:
D>>Это не задача. Это выглядящие бессмысленными упражнения с записыванием значений в непонятные участки памяти, очевидно, доступные глобально.
S>Я считаю, что начинать надо с малого. Лично мне не очень понятно, как добиться на Хаскелле решения такой простой проблемы и (серьезно!) хочу его увидеть.
Что за проблема-то?
S>Оно не такое уж и бессмысленное. У меня при программировании часто возникает код, подобный написанному. Хочу понять, как получить его аналог в функциональных языках.
Лови:
doIt = do
increment vA
vB ! vA --> vD *= 10
vF vG --> vH $= 0
Монады и операторы — твои друзья.
S>Некоторые тут, кстати, заявляют, что Хаскель лучший императивный язык...
Это не тут.
Это в Майкрософте.
Yours truly, Serguey Zefirov (thesz NA mail TOCHKA ru)
Y>>Пойми я тоже не хочу никого унизить, просто тут уже сразу видно невооруженным взглядом, отсутствие какой-либо базы, а без базы, тебе никто не сможет даже объяснить, почему ты не прав.
S>...решение этой задачи на Хаскелле. Особенно интересно в многопоточном варианте.
Приведенноё мной решение будет работать и с несколькими потоками тоже.
S>Если, конечно, наличествующая у вас база это позволяет сделать, а не просто утверждать, что "глобальные переменные это плохо".
sendval ixlist inp v = do
modindex <- nodemodindex node $ take 2 ixlist
asend (oIndexes group fullixlist++oNode node) modindex arity (inp,v)
where
ixlen = length ixlist
fullixlist
| ixlen == 3 =
sum ixlist:ixlist
-- sum (take 2 ixlist):ixlist
-- 0:ixlist
| otherwise = ixlist
node = inpnode inp
group = nodegroup node
arity = inparity inp
nodeS dimn x y ix@[_,i,j,k] = do
r <- fadd x y
c <- ilt k dimn
aif c
(do
k1 <- iadd k 1
sendval [i,j,k1] SInputX r
)
(sendval [i,j] CInputX r)
Сделай-ка аналогичный "ассемблер" на твоём любимом ЯП. Чтобы не менее удобно было — чтобы можно было определять свои "подпрограммы" и тп.
Yours truly, Serguey Zefirov (thesz NA mail TOCHKA ru)
Здравствуйте, VoidEx, Вы писали:
VE>Вот вам полный прикол:
Большое спасибо, это очень интересно.
То есть созданные в одной функции монадические переменные можно передавать в другие функции, но не делать глобальными. То есть для честного решения того примера в функцию DoIt() надо передать 7 переменных, чего народ ужаснулся. А так же передавать их во всю цепочку функций, из которых DoIt() вызывается. Меня сейчас будут пинать, но, признаться, в ряде случаев вариант с глобальными переменными мне нравится больше.
А можно описывать все функции в модуле как where-блок к блоку do в функции main, это не создат ли эффекта, что порожденные в блоке do переменные станут типа глобальными для этих функций?
VE>
VE>main = do
VE> chan <- newChan :: IO ByteReads
VE> s <- getChanContents chan
Насчет последней строчки. Она не лишняя? Или это очистка очереди от какого-то мусора?
VE>
VE>Кстати, я думал, что в Chan может быть один писатель, но не падает. Везёт, или писателей таки может быть куча?
Да вроде оно Control.Concurrent.Chan. По идее должно работать с несколькими потоками.
And if you listen very hard the alg will come to you at last.
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>з.ы. NGG>К дискуссии о "промышленном" решении можно будет вернуться, когда ФП окажется в массах и массы, пробежавшись по всем граблям n-раз, обобщат их до шаблонов, "запахов" и приниципов
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>Посмотрел на нашу битву реализаций и решил-таки (благо новый год и есть свободное время) познакомиться с одним из языков формальных спецификаций (Z) поближе, что в итоге позволило взглянуть на поднятую тему под немного другим углом.
NGG>Прежде всего, предложенное обобщение слишком "обобщенное": для того, чтобы перейти к дизайну, а затем реализации, требуется принять ряд решений (наложить ограничения) от которых итоговое решение довольно сильно зависит. Эти ограничения не имеют никакой связи с языком программирования или "стилем" проектирования принятым в этом языке.
Я тоже смотрю на эту битву и недоумеваю, зачем вообще нужны такого рода обобщения.
Может дело в ява культуре, от которой я слишком далек, и подозреваю что из-за
относительной (даже к C++ и С#) бедности языка там без этого просто никуда.
Просто по моему опыту (C++) даже на довольно крупных проектах (сотни тысяч строк и
человеко-годы) такого рода обобщения практически бесполезны, когда нужна правка оказывается
что обобщенно не в ту сторону или проще заново написать пусть даже заплатку чем разбиратся
в слишком усложненом коде.
Ну и еще к разности культур, меня интересуют сейчас функциональные (а до этого динамические)
языки как инструмент для повышения производительности одиночки или небольшой группы.
Для этих целей такие языки очень хороши и по моему позволяют довольно сильно уменьшить
самую рутиную часть программирование — кодирование, так как по сути написание текста
на выразительных языках ближе к проектированию (или к конструированию)
чем к кодированию.
Здравствуйте, thesz, Вы писали:
S>>Еще одно мнение в копилку — ФП дает выигрыш на узком классе задач, связанных со сложной работой с динамическими структурами данных (списками, деревьями, etc) — на что оно, собственно, и заточено. T>Прошу прощения, но ФП ни под что конкретно не заточено. T>Это всё в глазах наблюдателя.
Я под ФП понимаю направление человеческой мысли, разработавшей вполне конкретные языки (Lisp, ML, Haskell, Erlang, etc) со вполне конкретными фичами. Чисто функциями без side-эффектов можно писать и на С++, но его к ФП не причисляют, так как там этих фич и соответствующего синтаксического сахара нет (автоматическое управление паматью, patter matching, lambda functions, curring, high-order functions, etc). Реальная заточка этих фич — работа с рекурентными типами данных.
Естественно, в других языках есть и другие заточки, например, на работу с массивами.
Правда да, есть еще APL-J-K... Но это уже несколько другая область человеческой мысли, уж относят ли ее к ФП или нет.
S>>А на том, что типично пишут программисты, оно особенно не нужно.
T>Какие программисты-то?
Да самые разные. Вот, например, по-видимому самый широкий класс приложений — написание GUI к базам данных. Что-то не слышал, чтобы их вообще кто-то пытался писать на ФЯ. А почему? А потому что разработанные в ФЯ фичи для таких приложений малополезны.
And if you listen very hard the alg will come to you at last.
T>doIt = do
T> increment vA
T> vB ! vA --> vD *= 10
T> vF vG --> vH $= 0
T>
T>Монады и операторы — твои друзья.
Еще нет, все никак не разберусь толком с монадами. Нельзя ли в паре слов описать, как эти vA, vB и т.д. попадают в функцию? Для простоты можно ограничиться только первой операцией.
And if you listen very hard the alg will come to you at last.
Здравствуйте, thesz, Вы писали:
T>Сделай-ка аналогичный "ассемблер" на твоём любимом ЯП. Чтобы не менее удобно было — чтобы можно было определять свои "подпрограммы" и тп.
Могу предложить только интерпретатор Пролога, реализованный на Турбо Ассемблере, и то только в виде бинарника (сорцы утеряны). Смотреть в IDA. http://eoda.by.ru/Data/CrackMes/aliencpu.rar
And if you listen very hard the alg will come to you at last.
S>То есть для честного решения того примера в функцию DoIt() надо передать 7 переменных, чего народ ужаснулся. А так же передавать их во всю цепочку функций, из которых DoIt() вызывается. Меня сейчас будут пинать, но, признаться, в ряде случаев вариант с глобальными переменными мне нравится больше.
Если делать такие функции, то лучше императивный подход.
Прочтите ещё раз изначальную задачу, Вы решили (точнее предложили решение) её и так с кучей глобальных переменных, я их убрал. Возникает мысля, что если писать в ФП стиле, Вам просто не понадобится передавать по 7 переменных, не так ли?
Кстати, хочу посмотреть на это же на Вашем любимом языке, а то будет нечестно
Желательно императивным подходом (т.е. не мой код переписать на любой другой язык, а сделать так, как в этом языке обычно принято).
VE>>
VE>>main = do
VE>> chan <- newChan :: IO ByteReads
VE>> s <- getChanContents chan
S>Насчет последней строчки. Она не лишняя? Или это очистка очереди от какого-то мусора?
VE>>
Этой строкой я беру ленивый список всех попадающих в канал значений, который затем передаю в reader. Так же можно делать с файлами, например. reader, как можно заметить, понятия не имеет ни о каких Chan'ах, что даёт нам ещё одну степень абстракции. Я мог и reader'а-то не писать, а сразу написать, что мне со списком делать.
Здравствуйте, VoidEx, Вы писали:
VE>Кстати, я думал, что в Chan может быть один писатель, но не падает. Везёт, или писателей таки может быть куча?
В канал может писать кто угодно, в чей он области видимости, главное что бы тип записываемых данных подошёл. Фактически это же просто очередь с атомарным доступом (как у MVar)...
Здравствуйте, subdmitry, Вы писали:
S>Здравствуйте, deniok, Вы писали:
D>>Это не задача. Это выглядящие бессмысленными упражнения с записыванием значений в непонятные участки памяти, очевидно, доступные глобально.
S>Я считаю, что начинать надо с малого. Лично мне не очень понятно, как добиться на Хаскелле решения такой простой проблемы и (серьезно!) хочу его увидеть.
S>Оно не такое уж и бессмысленное. У меня при программировании часто возникает код, подобный написанному. Хочу понять, как получить его аналог в функциональных языках.
А нельзя ли поподробнее? С описанием типов всех этих A, B, C, D...
S>Некоторые тут, кстати, заявляют, что Хаскель лучший императивный язык...
Уточнение: не лучший, а самый прекрасный в мире (Haskell is the world's finest imperative language)...
T>>vA = ...что-то там такое...
T>>increment var = do { x <- getV var; setV var (x+1) }
T>>doIt = do
T>> increment vA
T>> vB ! vA --> vD *= 10
T>> vF vG --> vH $= 0
T>>
T>>Монады и операторы — твои друзья. S>Еще нет, все никак не разберусь толком с монадами. Нельзя ли в паре слов описать, как эти vA, vB и т.д. попадают в функцию? Для простоты можно ограничиться только первой операцией.
vA — это название твоей A. В Хаскеле с большой буквы называются конструкторы данных.
То, что increment пришлось определять в отличии от встроенного ++, не беда совершенно.
Yours truly, Serguey Zefirov (thesz NA mail TOCHKA ru)
Здравствуйте, geniepro, Вы писали:
G>В канал может писать кто угодно, в чей он области видимости, главное что бы тип записываемых данных подошёл. Фактически это же просто очередь с атомарным доступом (как у MVar)...
Спасибо, просто у меня почему-то было ощущение, что это один писатель, много читателей. Не знаю, откуда взялось [ощущение].
S>>>Еще одно мнение в копилку — ФП дает выигрыш на узком классе задач, связанных со сложной работой с динамическими структурами данных (списками, деревьями, etc) — на что оно, собственно, и заточено. T>>Прошу прощения, но ФП ни под что конкретно не заточено. T>>Это всё в глазах наблюдателя.
S>Я под ФП понимаю направление человеческой мысли, разработавшей вполне конкретные языки (Lisp, ML, Haskell, Erlang, etc) со вполне конкретными фичами.
Теперь тебе осталось выяснить, как же понимает ФП весь остальной мир.
S>Чисто функциями без side-эффектов можно писать и на С++, но его к ФП не причисляют, так как там этих фич и соответствующего синтаксического сахара нет (автоматическое управление паматью, patter matching, lambda functions, curring, high-order functions, etc).
Вот в лиспе нет pattern matching и currying. Там даже tail call optimization нет. Он всё ещё ФЯ?
(с моей точки зрения, кстати, не так, чтобы особо
S> Реальная заточка этих фич — работа с рекурентными типами данных.
По-моему, здесь мы имеем дело с так называемым non-sequitur.
С логическим скачком, иными словами.
Не видно, как из перечисленных тобой свойств ФП следует заточенность под работу с рекурсивными типами данных.
S>Естественно, в других языках есть и другие заточки, например, на работу с массивами.
А в ФП нет? Почему? "Single Assignment C" does ring any bell?
S>>>А на том, что типично пишут программисты, оно особенно не нужно.
T>>Какие программисты-то?
S>Да самые разные. Вот, например, по-видимому самый широкий класс приложений — написание GUI к базам данных. Что-то не слышал, чтобы их вообще кто-то пытался писать на ФЯ.
По-моему, ты не слышал, как кто-либо вообще хоть что-нибудь писал на ФЯ.
S>А почему? А потому что разработанные в ФЯ фичи для таких приложений малополезны.
А с моей точки зрения потому, что уровень писателей таких приложений в своей массе весьма низок. Потому, что платят мало, потому, что средний наниматель таких программистов сам невысокого уровня.
Yours truly, Serguey Zefirov (thesz NA mail TOCHKA ru)
T>>Сделай-ка аналогичный "ассемблер" на твоём любимом ЯП. Чтобы не менее удобно было — чтобы можно было определять свои "подпрограммы" и тп. S>Могу предложить только интерпретатор Пролога, реализованный на Турбо Ассемблере, и то только в виде бинарника (сорцы утеряны). Смотреть в IDA. S>http://eoda.by.ru/Data/CrackMes/aliencpu.rar
Не уворачивайся. Давай исходник "ассемблера" на твоем ИЯП.
Потрудись немного, уважь старика Зефирова, всё равно сейчас делать нечего.
Yours truly, Serguey Zefirov (thesz NA mail TOCHKA ru)
Здравствуйте, VoidEx, Вы писали:
VE>Здравствуйте, geniepro, Вы писали:
G>>В канал может писать кто угодно, в чей он области видимости, главное что бы тип записываемых данных подошёл. Фактически это же просто очередь с атомарным доступом (как у MVar)...
VE>Спасибо, просто у меня почему-то было ощущение, что это один писатель, много читателей. Не знаю, откуда взялось [ощущение].
Здравствуйте, thesz, Вы писали:
T>Вот в лиспе нет pattern matching и currying. Там даже tail call optimization нет. Он всё ещё ФЯ?
Ну вапще-то используя библиотеки, можно в лисп добавить и паттерн-матчинг, и карринг. А хвостовая рекурсия прямо указана в стандарте схемы, да и в CL тоже есть...
....
T>Потрудись немного, уважь старика Зефирова, всё равно сейчас делать нечего.
эх, а я вот и не заметил эти новогодние дни, потому как отдыхал всего пару дней...
T>>Вот в лиспе нет pattern matching и currying. Там даже tail call optimization нет. Он всё ещё ФЯ? G>Ну вапще-то используя библиотеки, можно в лисп добавить и паттерн-матчинг, и карринг. А хвостовая рекурсия прямо указана в стандарте схемы, да и в CL тоже есть...
"Я так и думал." (C) Администратор из Half-Life 2.
Yours truly, Serguey Zefirov (thesz NA mail TOCHKA ru)
Здравствуйте, thesz, Вы писали:
T>Вот в лиспе
Каком?
T>нет pattern matching
Могу про scheme сказать. В макросах матчинг есть по стандарту. Для матчинга в "рантаймовом коде" есть пара реализаций на макросах (с минимальными различиями в интефейсе), которые в нормальных имплементациях схемы доступны из коробки. Так что можно считать, что в схеме есть матчинг.
T>и currying.
Если очень хочется — можно на макросах сделать. Конечно, будет не так красиво, как в хаскеле, но частичное применение в любом случае не стыкуется красиво с переменным числом аргументов.
Там даже tail call optimization нет.
В схеме есть. Более того, там все вызовы в итоге получаются хвостовыми.
Здравствуйте, thesz, Вы писали:
S>>Чисто функциями без side-эффектов можно писать и на С++, но его к ФП не причисляют, так как там этих фич и соответствующего синтаксического сахара нет (автоматическое управление паматью, patter matching, lambda functions, curring, high-order functions, etc). T>Вот в лиспе нет pattern matching и currying. Там даже tail call optimization нет. Он всё ещё ФЯ?
В мире приято считать, что да.
В общем-то я не имел в виду, что этот список обязательно должен весь присутствовать в языке, чтобы его считать функциональным.
S>> Реальная заточка этих фич — работа с рекурентными типами данных. T>Не видно, как из перечисленных тобой свойств ФП следует заточенность под работу с рекурсивными типами данных.
Ну по крайней мере первые две это явно оно. А остальные... часто они у тебя используются применительно к нединамическим типам данных?
S>>Естественно, в других языках есть и другие заточки, например, на работу с массивами. T>А в ФП нет? Почему? "Single Assignment C" does ring any bell?
Синтаксическая поддержка может и есть. Другой вопрос, насколько оно эффективно компилируется.
Хотя в том же ML код для массивов наверное неплохой.
S>>А почему? А потому что разработанные в ФЯ фичи для таких приложений малополезны.
T>А с моей точки зрения потому, что уровень писателей таких приложений в своей массе весьма низок. Потому, что платят мало, потому, что средний наниматель таких программистов сам невысокого уровня.
Языки и средства разработки изобретают совсем не те люди, которые ими un mass пользуются. И у первых уровень отнюдь не низок. Если бы они чувствовали, что ФП дает, ну скажем, 50% выигрыша по времени разработки, его бы уже давно совали бы во все RADы. Но нет, реальность такова, что туда суют даже такие недоязыки как Паскаль или Бэйсик (языки без обобщенного программирования, брр), и ничего, для решения типичных программистских задач они оказываются вполне даже ничего.
And if you listen very hard the alg will come to you at last.
Здравствуйте, BulatZiganshin, Вы писали: BZ>я предлагаю начинать с попыток "учеников" самим решить предлагаемые задачи, и затем демонстрации "учителями" их решений
А продолжать будем? А то что-то дискуссия уехала куда-то.
Здравствуйте, thesz, Вы писали:
S>>Могу предложить только интерпретатор Пролога, реализованный на Турбо Ассемблере, и то только в виде бинарника (сорцы утеряны).
T>Не уворачивайся. Давай исходник "ассемблера" на твоем ИЯП.
Зря ты так, интерпретатор Пролога намного интереснее. 300 байт интерпретатора + 300 байт кода к нему. Один человек смог сломать, у него ушло 3 недели.
T>Потрудись немного, уважь старика Зефирова, всё равно сейчас делать нечего.
Я что-то не очень понимаю проблематику, зачем на ЯВУ реализовывать ассемблер, обычно делают наоборот. Ну так, навскидку
#define add(a,b) (a)+=(b);
И дальше в таком духе. Не подходит?
And if you listen very hard the alg will come to you at last.
Здравствуйте, subdmitry, Вы писали:
S>>>А почему? А потому что разработанные в ФЯ фичи для таких приложений малополезны.
На мой взгляд, весь вопрос в архитектуре приложения. Если писать код изначально в стиле C[whatever] — и потом пытаться перевести его напрямую на хаскель (ML, лисп и т.п.) — ничего путного не выйдет (равно как и в обратную сторону). Поэтому вопрос не в том, как в хаскеле работать со, скажем, глобальным состоянием, а как реализовывать на хаскеле те задачи, для который в C[whatever] используется глобальное состояние. Например, есть концепция реактивного (reactive), которая позволяет отказаться от глобального состояния в программах, ориентированных на GUI (сам я с этим всем толком не знаком — просто предлагаю ознакомиться ради интереса).
S>Языки и средства разработки изобретают совсем не те люди, которые ими un mass пользуются. И у первых уровень отнюдь не низок.
Есть мнение, что низок уровень вторых. Ничего категоричного утверждать не хочу, ибо сам на C# на жизнь зарабатываю
S>Я что-то не очень понимаю проблематику, зачем на ЯВУ реализовывать ассемблер, обычно делают наоборот. Ну так, навскидку S>#define add(a,b) (a)+=(b); S>И дальше в таком духе. Не подходит?
Не совсем.
Ты изучи проблему-то, изучи.
Yours truly, Serguey Zefirov (thesz NA mail TOCHKA ru)
Здравствуйте, thesz, Вы писали:
VE>>>Спасибо, просто у меня почему-то было ощущение, что это один писатель, много читателей. Не знаю, откуда взялось [ощущение]. G>>Я как-то баловался
-- сто тыщ потоков в один канал писать заставяля. А вот 300 тыщ потоков уже, блин, своппинг вызывают... Да, не Ерланг, всё-таки...
T>А у Эрланга таких проблем нет?
T>То есть, он не свопится, когда у нас есть 300 тысяч потоков? (каждый по (327+233)*word_size, то есть, 1.2К)
Откуда ты плюс взял? В источнике вроде:
327 words when spawned including a heap of 233 words.
Да и даже 1.2к * 300000 = 360 М, вроде не так чтоб много
S>>> Реальная заточка этих фич — работа с рекурентными типами данных. T>>Не видно, как из перечисленных тобой свойств ФП следует заточенность под работу с рекурсивными типами данных. S>Ну по крайней мере первые две это явно оно. А остальные... часто они у тебя используются применительно к нединамическим типам данных?
Это лямбды, карринг, ФВП.
Они у меня используются часто. Только я не понимаю, что такое "нединамические типы данных".
S>>>Естественно, в других языках есть и другие заточки, например, на работу с массивами. T>>А в ФП нет? Почему? "Single Assignment C" does ring any bell? S>Синтаксическая поддержка может и есть. Другой вопрос, насколько оно эффективно компилируется. S>Хотя в том же ML код для массивов наверное неплохой.
SAC бьёт Фортран. Наверное, они неплохо компилируют.
T>>А с моей точки зрения потому, что уровень писателей таких приложений в своей массе весьма низок. Потому, что платят мало, потому, что средний наниматель таких программистов сам невысокого уровня. S>Языки и средства разработки изобретают совсем не те люди, которые ими un mass пользуются. И у первых уровень отнюдь не низок.
Ты мне про компиляторщиков не говори. Я с ними близко общался на протяжении полутора лет.
— Это надо писать на плюсах, так удобней, чем на Си.
— Но это же можно и на ФЯ, например, на Хаскеле!
— На Хаскеле никто не пишет, все пишут на Си или Си++. И вообще, не мешай работать, ты компиляторы никогда не писал и ничего про их написание не знаешь.
Дословный диалог, буквально.
Так что ты мне про уровень не говори, повторюсь. Я этот уровень лбом ощутил.
Yours truly, Serguey Zefirov (thesz NA mail TOCHKA ru)
Здравствуйте, Mr.Cat, Вы писали:
T>>и currying. MC>Если очень хочется — можно на макросах сделать. Конечно, будет не так красиво, как в хаскеле, но частичное применение в любом случае не стыкуется красиво с переменным числом аргументов.
(cut cons (+ a 1) <>) is the same as (lambda (x2) (cons (+ a 1) x2))
(cut list 1 <> 3 <> 5) is the same as (lambda (x2 x4) (list 1 x2 3 x4 5))
(cut list) is the same as (lambda () (list))
(cut list 1 <> 3 <...>) is the same as (lambda (x2 . xs) (apply list 1 x2 3 xs))
(cut <> a b) is the same as (lambda (f) (f a b))
И с переменным числом аргументов выглядит не так уж и плохо.
Кстати, почему в Haskell не сделают что-то типа
map _ [1,2,3] as \f -> map f [1,2,3]
Ну т.е. аналогично scheme.
Здравствуйте, thesz, Вы писали:
Все, все, понял, Вы про CL. Мне он, кстати, тоже как-то не понравился.
T>Схема — не лисп.
Простите уж мое занудство.
Scheme is a statically scoped and properly tail-recursive dialect of the Lisp programming language invented by Guy Lewis Steele Jr. and Gerald Jay Sussman.
Здравствуйте, deniok, Вы писали:
B>>Кстати, почему в Haskell не сделают что-то типа B>>map _ [1,2,3] as \f -> map f [1,2,3] B>>Ну т.е. аналогично scheme.
D>А зачем, если можно с той же целью написать D>
D>flip map [1,2,3]
D>
Я знал, что ответ будет таким Ну да ладно...
Затем, что
1. flip map [1,2,3] труднее воспринять чем map _ [1,2,3] (я понимаю, что это спорно)
2. существует функции у которых количество параметров больше двух
3. можно было бы работать с tuple: fun (_,1) _ ===> \x y -> fun (x,1) y
4. и может даже так: putStrLn $ "A=" ++ (show _) ++ "B=" ++ (show _) ===> \a b -> putStrLn $ "A=" ++ (show a) ++ "B=" ++ (show b)
На мой взгляд, пункты 3 и 4 ломают семантику языка.
B>3. можно было бы работать с tuple: fun (_,1) _ ===> \x y -> fun (x,1) y B>4. и может даже так: putStrLn $ "A=" ++ (show _) ++ "B=" ++ (show _) ===> \a b -> putStrLn $ "A=" ++ (show a) ++ "B=" ++ (show b)
Как видно, андерскор вне паттерна превращается в жуткую волшебную палочку, т.е. (_, 1) — больше не тупл, а fun (_, 1) — не применение функции fun к выражению (_, 1). Я бы не хотел, чтобы в хаскеле появилось что-то подобное.
Кстати, я так и не удосужился познакомиться с Template Haskell — с его помощью разве нельзя сделать частичное применение, как в srfi-26?
Здравствуйте, Beam, Вы писали:
B>Я знал, что ответ будет таким Ну да ладно...
B>Затем, что B>1. flip map [1,2,3] труднее воспринять чем map _ [1,2,3] (я понимаю, что это спорно)
map `flip` [1,2,3]
или (для любителей явных операторов) приводим к тому, что просили
(***) = flip
map *** [1,2,3]
B>2. существует функции у которых количество параметров больше двух
А тут отложенное неименованное связывание легко рушит семантику
B>3. можно было бы работать с tuple: fun (_,1) _ ===> \x y -> fun (x,1) y
Да-да, вот тут и рушит Представь, что fun это flip и вспомни что вычисления идут лениво слева направо...
Здравствуйте, Mr.Cat, Вы писали:
MC>Здравствуйте, Beam
MC>На мой взгляд, пункты 3 и 4 ломают семантику языка.
B>>3. можно было бы работать с tuple: fun (_,1) _ ===> \x y -> fun (x,1) y B>>4. и может даже так: putStrLn $ "A=" ++ (show _) ++ "B=" ++ (show _) ===> \a b -> putStrLn $ "A=" ++ (show a) ++ "B=" ++ (show b)
MC>Как видно, андерскор вне паттерна превращается в жуткую волшебную палочку, т.е. (_, 1) — больше не тупл, а fun (_, 1) — не применение функции fun к выражению (_, 1). Я бы не хотел, чтобы в хаскеле появилось что-то подобное.
Ну я и не настаивал, спросил почему не делают. В принципе, не обязательно использовать подчеркивание.
А вообще, я согласен, что это усложнит язык. Проще и понятнее написать лишнюю функцию в where.
D>или (для любителей явных операторов) приводим к тому, что просили D>
D>(***) = flip
D>map *** [1,2,3]
D>
Оригинально.
B>>2. существует функции у которых количество параметров больше двух D>А тут отложенное неименованное связывание легко рушит семантику
B>>3. можно было бы работать с tuple: fun (_,1) _ ===> \x y -> fun (x,1) y D>Да-да, вот тут и рушит Представь, что fun это flip и вспомни что вычисления идут лениво слева направо...
Вот здесь не понял. Ведь это ничем не отличается от
let myFun x y = fun (x,1) y in myFun 1 2
И вроде, здесь нет проблем никаких
T>>>>vA = ...что-то там такое... S>Ага, так вот они, глобальные переменные в Хаскелле! Все, понял. Действительно, императивные фичи можно реализовать, что радует.
Ну, это уж ты как-то совсем того.
Есть такой сайт, Lambda the Ultimate, назван по серии статей Гая Стила (Guy Steele), одного из разработчиков Java.
В этой серии одна из статей называется "Lambda the Ultimate Imperative". AFAIK, там рассказывается про императивное программирование с точки зрения ЛИ.
Могу ошибаться, правда.
Yours truly, Serguey Zefirov (thesz NA mail TOCHKA ru)
B>>>3. можно было бы работать с tuple: fun (_,1) _ ===> \x y -> fun (x,1) y D>>Да-да, вот тут и рушит Представь, что fun это flip и вспомни что вычисления идут лениво слева направо...
B>Вот здесь не понял. Ведь это ничем не отличается от B>let myFun x y = fun (x,1) y in myFun 1 2 B>И вроде, здесь нет проблем никаких
У тебя два неименованных места для будущего связывания
myComb = fun (_,1) _
Если потом вызвать
myComb x y
то связывание просто по месту не пройдет, нужны имена (обычная лямбда) или номера (индексы де Бр(ой)(ау)на). Поскольку если fun у тебя, например, такой
fun (a,b) c = c a
то вычисление пойдет так
myComb x y ~>
(fun (_,1) _) x y ~>
(_ _) x y ~>
x y
-- ой мама биндеры переставились, должно-то быть y x
Здравствуйте, thesz, Вы писали:
T>Сделай-ка аналогичный "ассемблер" на твоём любимом ЯП. Чтобы не менее удобно было — чтобы можно было определять свои "подпрограммы" и тп.
Здравствуйте, deniok, Вы писали:
D>Здравствуйте, Beam, Вы писали:
B>>Кстати, почему в Haskell не сделают что-то типа B>>map _ [1,2,3] as \f -> map f [1,2,3] B>>Ну т.е. аналогично scheme.
D>А зачем, если можно с той же целью написать D>
Здравствуйте, Beam, Вы писали:
B>Кстати, почему в Haskell не сделают что-то типа B>map _ [1,2,3] as \f -> map f [1,2,3]
я слышал, что проблема в том, что непонятно, где вставлять \_. например в твоём примере:
map (\f -> f) [1,2,3]
или
\f -> map f [1,2,3]
или
(\f -> map f) [1,2,3]
Здравствуйте, thesz, Вы писали:
T>>>То есть, он не свопится, когда у нас есть 300 тысяч потоков? (каждый по (327+233)*word_size, то есть, 1.2К)
К>>Откуда ты плюс взял?
T>Просмотрел. Но я нашёл поинтересней.
Ммм, Сергей, думаю стоит внимательней читать, ну какое отношение стэк эмулятора имеет к памяти тех процессов, которые в нём крутятся?
И чуть ниже смотрим:
+h Size
Sets the default heap size of processes to the size Size.
Вот этот другой (а не +a size) параметр задаёт размер хипов процессов.
Здравствуйте, Tonal-, Вы писали:
T>1. Зачитать это в T>
T>data Tree = Leaf Int String | Tree Int String [Tree]
T>
T>где Int — номер строки, String — строка без начатльных и конечных пробелов
во-первых, проще
data Tree = Tree Int String [Tree]
во-вторых, в постановке задачи ошибка попробуй сам описать, какое дерево должно быть построено по твоим входным данным
task1 = zip [1..] -- нумеруем строки
>>> makeTrees -- формируем список деревьев
makeTrees = groupFrom (not.isSpace.head.snd) -- разбиваем на группы, начинающиеся со строки без отступа
-- в каждой из групп первую строчку используем для заголовка дерева
>>> map ( \((linenum,string):xs) -> Tree linenum string
-- а в оставшихся строках отрезаем первые два пробела и
-- рекурсивно изготовляем из них список деревьев
(makeTrees (map (drop 2) xs)))
T>2. Сделать чтение устойчивым к ошибкам пропуска уровня типа: T>
Действительно "мастер-класс"!
У меня кода получилось в несколько раз больше.
BZ>не совсем понятно, что значит сделать устойчивым? на мой взгляд, нужно просто подавать жалобу из groupFrom: BZ>
BZ>groupFrom crit [] = []
BZ>groupFrom crit (x:xs) | not (crit x) = error"ах вы суки эдакие!!!"
BZ>groupFrom crit (x:xs) = let (l1:l2) = break crit xs
BZ> in (x:l1) : groupFrom crit l2
BZ>
Я имел в виду, что дерево нужно строить дальше, хотя кончно так и нужно было написать в постановке.
Хотя groupFrom довольно просто изменить под это:
groupFrom crit [] = []
groupFrom crit (x:xs) =
let
(l1:l2) = break crit xs
grp x l | not (crit x) = (fst x, "(!)") : x : l
grp x l = x:l
in grp x l1 : groupFrom crit l2
T>>The default suggested stack size is 16 kilowords T>>Оба-на! К>Ммм, Сергей, думаю стоит внимательней читать, ну какое отношение стэк эмулятора имеет к памяти тех процессов, которые в нём крутятся?
А стек самого процесса, который крутится, где задаётся?
Как я понимаю, он у каждого "легкого процесса" должен быть свой.
К>И чуть ниже смотрим: К>
К>+h Size
К> Sets the default heap size of processes to the size Size.
К>Вот этот другой (а не +a size) параметр задаёт размер хипов процессов.
Я про стек начал говорить. Стек для каждого из процессов, наверное, в хип не входит.
Yours truly, Serguey Zefirov (thesz NA mail TOCHKA ru)
Здравствуйте, thesz, Вы писали:
T>>>The default suggested stack size is 16 kilowords T>>>Оба-на! К>>Ммм, Сергей, думаю стоит внимательней читать, ну какое отношение стэк эмулятора имеет к памяти тех процессов, которые в нём крутятся?
T>А стек самого процесса, который крутится, где задаётся?
T>Как я понимаю, он у каждого "легкого процесса" должен быть свой.
А фиг знет где конкретно, тупо просмотром через запуск процесса и вызов process_info/1 выдаёт 8 слов. Но сдаётся мне, что они как раз в те предыдущие 327 как раз входят.
А как можно изменить этот код чтобы в в начале каждой строки отображался linenum нода?
T>>4. Отфильтровать дерево, оставив только ветви содержащие дубликаты и сами дубликаты.
И опять я не точно выразился.
Дубликатами считаются одинаковые строки одного родителя.
Например:
Животные
рыбы
щука
карась
щука
После фильтрации должно получится:
Животные
рыбы
щука
щука
Как подправить, сходу не соображу.
Кстати, в моей инсталяции ghc 6.10.1 для винды, в документации нет упоминания mapCatMaybes и описания модуля Maybes тоже нет. Зато есть пустая страница для модуля Maybe
T>>А стек самого процесса, который крутится, где задаётся? T>>Как я понимаю, он у каждого "легкого процесса" должен быть свой. К>А фиг знет где конкретно, тупо просмотром через запуск процесса и вызов process_info/1 выдаёт 8 слов. Но сдаётся мне, что они как раз в те предыдущие 327 как раз входят.
Короче говоря, ничего не понятно.
Ты бы провёл эксперимент с 300K отправителей.
Yours truly, Serguey Zefirov (thesz NA mail TOCHKA ru)
Здравствуйте, Tonal-, Вы писали:
T>Кстати, в моей инсталяции ghc 6.10.1 для винды, в документации нет упоминания mapCatMaybes и описания модуля Maybes тоже нет. Зато есть пустая страница для модуля Maybe
По-моему функции mapCatMaybes в стандартной библиотеке нет сейчас и не было раньше.
А Maybe перенесли в Data.Maybe. Но модуль Maybe тоже будет работать, так как просто импортирует Data.Maybe
Здравствуйте, Tonal-, Вы писали:
T>А как можно изменить этот код чтобы в в начале каждой строки отображался linenum нода?
show?
T>>>4. Отфильтровать дерево, оставив только ветви содержащие дубликаты и сами дубликаты. T>Как подправить, сходу не соображу.
строй список дубликатов локально
T>Кстати, в моей инсталяции ghc 6.10.1 для винды, в документации нет упоминания mapCatMaybes и описания модуля Maybes тоже нет. Зато есть пустая страница для модуля Maybe
это из модуля Data.Maybe, я просто держу на диске исходники
B>-- универсальная функция, группирует список пар (ключ, значение) по ключу, возвращая список пар (ключ, [значения для этого ключа])
B>-- например groupByFst [(1,2), (1,3), (2,5), (1,4), (2,6)] возвращает [ (1,[2,3,4]), (2,[5,6]) ]
B>-- записываем слово в файл "?.txt" если слово содержит букву ?
B>-- дополнительно слова, длиной 3 пишем во временный файл
B>-- дополнительно слова, начинающиеся с гласной буквы пишем в консоль
NGGU> type Targets = String -> [Target]
NGGU> targets3 :: Targets
B>targets3 word = byContain word ++ byLen3 word ++ byVowel word
B> where
B> byContain letters = [File $ s:".txt" | s <- letters]
B> byLen3 letters | length letters == 3 = [TempFile]
B> | otherwise = []
B> byVowel letters | head letters `elem` ['e','u','i','o','a'] = [Console]
B> | otherwise = []
B>
B>Обработку мы настраиваем двумя функциями (передаются в качестве параметра processWord): B>1. targets — по слову говорит, куда это слово надо записать (в консоль, в конкретный файл, никуда) NGGU> (Кстати, так задумано, что слово с 3 буквами 'e' попадает в файл с именем 'e' трижды?) B>2. process — выполняет саму обработку группы слов
Во всех предложенных решениях, кроме, разве что совсем не кастомизируемых, выбор списка для представления элементов группы (а именно его иммутабельность) приводит к необходимости создания конструкции вида [(ID, String)], где ID — идентификатор группы (от вырожденных случаев где ID — имя файла, до сложных — где ID содержит информацию о способе обработки), и необходимости эту конструкцию преобразовывать к виду — [(ID, [String])].
Это иллюстрация того, как выбор "архитектурного решения" (дизайн, если хотите) оказывается взависимости от выбора языка реализации, хотя, казалось бы, такого быть не должно :)
Есть, так же, довольно любопытное мнение, озвучиваемое в л-ре посвящённой ооп, согласно которому, функциональная декомпозиция имеет существенный недостаток перед оо-декопозицей. Утверждается, что при ф-декомпозиции разбиение на подзадачи зависит от выбора "главной функции" системы (в нашей задаче, честно говоря, выбор этой функции очевиден :)) и нет никакой гарантии, что при реализации новой функции получится использовать подзадачи полученные входе первоначального решения -> либо часть функциональности будет повторена, либо потребуется провести декомпозицию всего решения заново, чтобы выделить общие подзадачи...
С этим утверждением можно согласиться, когда речь идёт о "классических" процедурных языках, но в случае с современными функциональными языками, где есть фвп и прочии прелести, можно и поспорить. Чтобы опровергнуть его, нужно привести какие-то доводы в пользу того, что при декомпозиции будут полученны подзадачи имеющие самостоятельное значение для предметной области над которой определена исходная задача (а не для вычислителя). В случае ОО-декомпозиции, этого пытаются достич моделируя сущности/концепции (называйте как хотите) из предметной области в виде объектов и классов объектов, а отнощения между ними — в виде методов классов, статических методов или "синтетических" объектов. В итоге образуется набор "кубиков" из которых затем строится решение конкретной задачи. Поскольку изменения в предметной области случаются реже, чем формулируются новые задачи, вероятность использовать при решении новой задачи существующие "кубики" достаточна велика, что, в каком-то виде, гарантирует устойчивость решения к изменениям в требованиях и упрощение реализации новых требований через повторное использование не только результатов анализа предметной области, но и имеющегося кода. Минусы тут тоже есть:
— во-первых, выделение сущностей и отношений между ними процесс "творческий" и если нет достаточного опыта или таланта, то результат может оказаться плачевным.
— во-вторых, не всегда "изменения в предметной области случаются реже, чем формулируются новые задачи" -> для таких областей нужен другой подход к проектированию.
— в-третьих, существует граница, когда написать решение с нуля проще, чем разбираться во всех имеющихся "кубиках" (это относится не только к ООП, но и к ФП :))
— в-четвёртых, с определённого уровня вложенности подзадачи перестают относиться к предметной области и превращаются в "борьбу" с техническими сложностями/особенностями языка разработки и вспомогательными библиотеками (стандартными/сторонними).
Тем не менее, худо-бедно с этими минусами справиться удаётся.
В случае с ФП всё не так прозрачно. Есть требуется решить "конкретную" задачу, то всё понятно. Задачу крошим на подзадачи и красиво расписываем. Если нужно создать основу для решения родственных задач — тоже понятно — формулируется "конкретная" задача, где описываются вариации, далее задача решается "обычным" путём. Всё хорошо, но только "точки вариации" должны быть описаны _заранее_ (в отличии ОО подхода, когда "все возможные" вариации задач закладываются в модели предметной области (в классах и методах) без попытки перечислить их _все_ явно).
Булат озвучил подход при котором ФЯ используется как средство для описания dsl, на котором затем решается задача. Этот подход отличается от разбиения задачи на подзадачи и может в теории (так же как и ООП — _в теории_ :)) дать гарантии "устойчивости" о которых выше шла речь, т.к. язык предметной области должен, по сути, отражать теже сущности и отношения между ними за которыми гоняется ООП, только в виде отличном от классов и методов (и может быть даже более красивым способом) и убирать на задний план "борьбу" языком и библиотеками. Правда, в этом случае возникает вопрос "как придумать хороший dsl"... но отложим его в сторону до лучших времен
А подумалось вот о чём: может если использовать идею о dsl, то "дизайн" несчастной задачи про слова не зависил бы от языка, как указывалось выше? Надо бы попробовать :)
Итак, нужно выбрать способ формального описания условия задачи, а затем реализовать преобразование формального описания в описание на яп.
Отталкиваться будем от того, что критерий разбивки слов на группы часто меняется, поэтому dsl должен позволять быстро реализовывать новые варианты. Можно сделать так:
Очевидно, что требуются примитивы для упрощения ввода "программы":
StartWith x -> WriteToFile x, x in ["a" .. "z"]
Сontains "c" | Сontains "d" -> WriteToFile "Contains 'c | d'"
Match (wordPrefix 3) | "paa" -> WriteToConsole "debug", WriteToFile "paa"
| prefix -> WriteToFile prefix
Почесав репу, можно добавить нотацию для конфигурирования процесса группировки:
StartWith x -> WriteToFile x {unique: true}, x in ["a" .. "z"]
-- ^^ в файл пишутся только уникальные строки (дубликаты игнорируются и в другие группы не попадают)
Сontains "c" | Сontains "d" {unique: true} -> WriteToFile "Contains 'c | d'"
-- ^^ матчатся только уникальные строки (дубликаты не матчатся и могут попасть в другие группы)
Match (wordPrefix 3) | "paa" -> WriteToConsole "debug", WriteToFile "paa"
| prefix -> WriteToFile prefix
Думаю, что такой-же синтаксис можно было бы, чуть-чуть изменив, свести к синтаксису хаскелл. А как насчёт ООЯ?
В java получилось бы что-то такое:
b = new RuleBuilder()
for(String x : new Range("a", "z")) {
b.addRule(new StartWith(x),
new WriteToFile(x).unique(true));
}
b.addRule(new Or(new Contains("c"), new Contains("d")).unique(true),
new WriteToFile("Contains 'c | d'"));
b.addRule(new WordPrefixRule(3) {
Action createDefaultAction(String prefix) {
return new WriteToFile(prefix);
}
}.add("paa", new WriteToConsole("debug"), new WriteToFile("paa")));
b.process(theData);
Конечному пользователю, конечно, не отдашь, но если бы была выгода от использования текстового dsl, то от этого чуда её тоже можно было бы поиметь.
B>На самом деле, то же можно повторить и в ООП :) Разный подход будет в "настройке". B>Думаю решение на ООП будет выглядеть чуть по сложнее, т.к., на мой взгляд, функцию легче охватить взглядом и понять, чем класс ООП. B>Не говоря уже о добавлении нескольких классов / интерфейсов.
От смотрящего многое зависит. Имхо, если классы/интерфейсы не с неба брались, то ситуация аналогична ситуации со скобками в лиспе: кто-то без них жить не может, кто-то принципиально не перевариавает :)
BulatZiganshin пишет:
> итак, попробуйте решить эти задачи: > > 1) есть список слов. надо записать в файл "a" все слова, начинающиеся на > букву 'a', в файл "b" — слова на 'b' и т.д. вплоть до z
Я что=то не вижу тут особенно функционального программирования.
Декларативное — при возможности делать destructuring строк — может быть.
Тут даже функций высших порядков не применить, кажется.
;;;; -*- Mode: Lisp; Syntax: Common-Lisp; Package: fp-train-1 -*-
(defpackage #:fp-train-1 (:use :cl))
(in-package #:fp-train-1)
(defun 1-st-letter (word)
(cond ((symbolp word) (char (symbol-name word) 0))
((stringp word) (char word 0))
((characterp word) word)
(t word)))
(let ((files nil))
(defun letter-file (letter)
(let ((file (cdr (assoc letter files))))
(if file
file
(let ((file (open (string letter) :direction :output)))
(setf files (acons letter file files))
file))))
(defun finish ()
(mapc (lambda (e) (close (cdr e))) files)))
(defun cast-words (word-list)
"Casts all words in WORD-LIST by files named A, B, ... Z according to their
first letter"
(unwind-protect
(mapc
(lambda (word)
(format (letter-file (1-st-letter word)) "~A~%" word))
word-list)
(finish)))
BZ>3-я задача: есть функция сжатия compress :: String -> String. ей на вход передаётся строка (или буфер и его размер — это непринципиально), а она возвращает сжатые данные. и есть аналогичная функция decompress. при этом они однопоточные. а у нас 4-процессорная машина. соответственно, нужно написать программу, читающую входные данные мегабайтными блоками, и упаковывающую "в 4 руки", и к ней программу распаковки
Гы, а что значит "однопоточные функции" без контекста языка программирования ?
Ты уж того, давай другую какую-то задачу. Если вся сложность данной в том, чтобы преодолеть нериэнтерабельность
данных двух функций, но не фиксирован язык программирования, то задача как-то очень странна.
BB>На плюсах. BB>Вариант №1. Do not optimize prematurely...
Плюсы не так и плохо выглядят. Конечно, тут упрощено сильно всё, но всё же.
Это ещё раз доказывает, что задача -- не для функционального языка. Функциональные
задачи на С++ решаются хреновенько -- не тот язык. Хотя можно и там.
BZ>>3-я задача: есть функция сжатия compress :: String -> String. ей на вход передаётся строка (или буфер и его размер — это непринципиально), а она возвращает сжатые данные. и есть аналогичная функция decompress. при этом они однопоточные. а у нас 4-процессорная машина. соответственно, нужно написать программу, читающую входные данные мегабайтными блоками, и упаковывающую "в 4 руки", и к ней программу распаковки
MZ>Гы, а что значит "однопоточные функции" без контекста языка программирования ?
MZ>Ты уж того, давай другую какую-то задачу. Если вся сложность данной в том, чтобы преодолеть нериэнтерабельность MZ>данных двух функций, но не фиксирован язык программирования, то задача как-то очень странна.
проблема в том, что эти функции задействуют только одно ядро, соответственно скорость сжатия получается вчестверо меньше, чем могло бы быть при задействовании всех ядер. соответственно, задача состоит в том, чтобы читать данные кусками по мегабайту, сжимать до 4 кусков одновременно, и затем записывать сжатые данные на диск
вот тесты мой собственной реализации этого процесса:
было:
D:\> Arc.exe create a enwik8 -m2 -t
Compressed 1 file, 100.000.000 => 26.576.090 bytes. Ratio 26.5%
Compression time 8.36 secs. Real time 8.92 secs, speed 11.212 kB/s
Testing time 10.69 secs. Real time 11.19 secs, speed 8.940 kB/s
стало:
C:\> Arc.exe create a enwik8 -m2 -t
Compressed 1 file, 100.000.000 => 26.576.090 bytes. Ratio 26.5%
Compression time 9.22 secs. Real time 2.63 secs, speed 37.965 kB/s
Testing time 14.12 secs. Real time 3.85 secs, speed 25.994 kB/s
BulatZiganshin пишет:
> проблема в том, что эти функции задействуют только одно ядро, > соответственно скорость сжатия получается вчестверо меньше, чем могло бы
Здравствуйте, MasterZiv, Вы писали:
MZ>BulatZiganshin пишет:
>> проблема в том, что эти функции задействуют только одно ядро, >> соответственно скорость сжатия получается вчестверо меньше, чем могло бы
MZ>Какие функции, какое ядро, не понимаю ничего.
а ты вообще когда-нибудь пробовал программировать для многоядерных процессоров?
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>Во всех предложенных решениях, кроме, разве что совсем не кастомизируемых, выбор списка для представления элементов группы (а именно его иммутабельность) приводит к необходимости создания конструкции вида [(ID, String)], где ID — идентификатор группы (от вырожденных случаев где ID — имя файла, до сложных — где ID содержит информацию о способе обработки), и необходимости эту конструкцию преобразовывать к виду — [(ID, [String])].
Вообще-то другого здесь и не дано. По своей сути — это задача классификации.
А классификация — это установление зависимости между элементами и классами. Отсюда появляется [(ID, element)]
Классификация не нужна сама по себе. Мы должны что-то сделать с группами. Отсюда появляется [(ID, [element])]
NGG>Это иллюстрация того, как выбор "архитектурного решения" (дизайн, если хотите) оказывается взависимости от выбора языка реализации, хотя, казалось бы, такого быть не должно
Мне непонятно, что понимается под дизайном. И непонятна иллюстрация "того, как выбор "архитектурного решения" (дизайн, если хотите) оказывается взависимости от выбора языка реализации".
По-моему, при обобщении задачи классификации, так или иначе, решения на любом языке придут к чему-то похожему выше.
Разница лишь в том, как эти данные будут представлены языке. Как список в ФЯ или как класс группа, агрегирующий свои элементы, в ООП.
NGG>Есть, так же, довольно любопытное мнение, озвучиваемое в л-ре посвящённой ооп, согласно которому, функциональная декомпозиция имеет существенный недостаток перед оо-декопозицей. Утверждается, что при ф-декомпозиции разбиение на подзадачи зависит от выбора "главной функции" системы (в нашей задаче, честно говоря, выбор этой функции очевиден и нет никакой гарантии, что при реализации новой функции получится использовать подзадачи полученные входе первоначального решения -> либо часть функциональности будет повторена, либо потребуется провести декомпозицию всего решения заново, чтобы выделить общие подзадачи... NGG>С этим утверждением можно согласиться, когда речь идёт о "классических" процедурных языках, но в случае с современными функциональными языками, где есть фвп и прочии прелести, можно и поспорить. Чтобы опровергнуть его, нужно привести какие-то доводы в пользу того, что при декомпозиции будут полученны подзадачи имеющие самостоятельное значение для предметной области над которой определена исходная задача (а не для вычислителя).
Про какие подзадачи идет речь? Вот например есть функция.
Где здесь подзадачи?. Эта функция решает обощенную задачу и определяет требования к классификатору и обработчику (аналог интерфейса).
А сами функции обработчик и классификатор будут написаны для каждой предметной области уже потом.
NGG>В случае ОО-декомпозиции, этого пытаются достич моделируя сущности/концепции (называйте как хотите) из предметной области в виде объектов и классов объектов, а отнощения между ними — в виде методов классов, статических методов или "синтетических" объектов. В итоге образуется набор "кубиков" из которых затем строится решение конкретной задачи. Поскольку изменения в предметной области случаются реже, чем формулируются новые задачи, вероятность использовать при решении новой задачи существующие "кубики" достаточна велика, что, в каком-то виде, гарантирует устойчивость решения к изменениям в требованиях и упрощение реализации новых требований через повторное использование не только результатов анализа предметной области, но и имеющегося кода.
В том-то и дело, что классы привязаны к предметной области, а вот функции нет (ну почти )
А чтобы отвязать их, появляются классы, которые имеют в себе лишь поведение (по сути функция), и классы хранящие только данные (параметры функций).
NGG>В случае с ФП всё не так прозрачно. Есть требуется решить "конкретную" задачу, то всё понятно. Задачу крошим на подзадачи и красиво расписываем. Если нужно создать основу для решения родственных задач — тоже понятно — формулируется "конкретная" задача, где описываются вариации, далее задача решается "обычным" путём. Всё хорошо, но только "точки вариации" должны быть описаны _заранее_ (в отличии ОО подхода, когда "все возможные" вариации задач закладываются в модели предметной области (в классах и методах) без попытки перечислить их _все_ явно).
Мне не совсем понятно, где вариации описаные заранее. Вон в том же примере "обработатьГруппы".
Описаны требования (интерфейсы), но не сами вариации. То же самое и в ООП. Не вижу я разницы.
Здравствуйте, Beam, Вы писали:
B>Вообще-то другого здесь и не дано. По своей сути — это задача классификации. B>А классификация — это установление зависимости между элементами и классами. Отсюда появляется [(ID, element)] B>Классификация не нужна сама по себе. Мы должны что-то сделать с группами. Отсюда появляется [(ID, [element])]
NGG>>Это иллюстрация того, как выбор "архитектурного решения" (дизайн, если хотите) оказывается взависимости от выбора языка реализации, хотя, казалось бы, такого быть не должно
B>Мне непонятно, что понимается под дизайном. И непонятна иллюстрация "того, как выбор "архитектурного решения" (дизайн, если хотите) оказывается взависимости от выбора языка реализации".
"Задача классификации" — это всё-таки не наша задача.
Наша задача состоит в том, чтобы "раскидать" данные по обработчикам согласно определённому набору критериев.
Причём "в лоб" (в императивном стиле) эта задача решается с использованием O(1) доп.памяти и оценкой O(N) (если исключить монстрообразные критерии, требующие хранения состояния и сложных вычислений).
И нигде не звучит требования построить список [(ID, element)] или список [(ID, [element])] в явном виде, что тянет за собой как минумум O(N)/O(NlogN) по памяти/времени и, думается, с "хорошей" С перед оценкой времени.
Вопрос: чем руководствовался "дизайнер" принимая решение так разбить задачу на подзадачи? Вариантов ответа, конечно же, можно придумать много, как оскорбительных, так и наоборот . Имхо, мысль течёт примерно так:
— "грязь" с выводом в файл скрываем за функцией String -> [String] -> IO(),
— значит в "чистом" коде нужно получить перечисление [(String, [String])]
— инкрементально такую структуру не построить (добавляя по слову), значит нужно промежуточное представление, которое можно строить быстро. Такая структура есть [(String, String)]
— ну, а дальше sortBy, который затягивает все слова в память и даёт N*logN
Т.е. декомпозиция (выделение подзадач) основывается на особенностях языка (грязь отдельно, иммутабельные структуры данных, особенности синтаксиса), а не на анализе задачи.
Можно возразить, что решение демонстрирует как красиво/лаконично/и т.п. писать на ФЯ. Ок, но это значит, что решение тем более построенно исходя из выразительных особенностей языка реализации, а не на основе анализа задачи... ч.т.д
B>По-моему, при обобщении задачи классификации, так или иначе, решения на любом языке придут к чему-то похожему выше. B>Разница лишь в том, как эти данные будут представлены языке. Как список в ФЯ или как класс группа, агрегирующий свои элементы, в ООП.
Я бы не стал утверждал, что в любом решении будет содержаться подзадача преобразовывать [(ID, element)] -> [(ID, [element])].
B>Про какие подзадачи идет речь? Вот например есть функция. B>
B>Где здесь подзадачи?. Эта функция решает обощенную задачу и определяет требования к классификатору и обработчику (аналог интерфейса). B>А сами функции обработчик и классификатор будут написаны для каждой предметной области уже потом.
Как это где?
1) Построить классификатор и обработчик в соответствии с условием задачи (далеко не факт, что это элементарно сделать).
2) разбитьНаГруппы используя классификатор
3) обработатьКаждуюГруппу используя обработчик
B>В том-то и дело, что классы привязаны к предметной области, а вот функции нет (ну почти ) B>А чтобы отвязать их, появляются классы, которые имеют в себе лишь поведение (по сути функция), и классы хранящие только данные (параметры функций).
"Функция" может быть отвязана от предметной области, только если она принадлежит "нижележащему" уровню абстракции/обвязке решения, н-р, привязана к collection api языка.
Такие функции не требуется откуда-то отвязывать, т.к. это либо не имеет смысла (н-р, map, filter, foldr в java всё равно удобнее делать через цикл), либо уже давно отвязано и помещено в стандартные или общеупотебимые библиотеки.
Среди причин, по которым могут появляться классы без состояния, я не нахожу ни одной похожей на "желание отвязаться от предметной области".
NGG>>Всё хорошо, но только "точки вариации" должны быть описаны _заранее_ (в отличии ОО подхода, когда "все возможные" вариации задач закладываются в модели предметной области (в классах и методах) без попытки перечислить их _все_ явно).
B>Мне не совсем понятно, где вариации описаные заранее. Вон в том же примере "обработатьГруппы". B>Описаны требования (интерфейсы), но не сами вариации. То же самое и в ООП. Не вижу я разницы.
Описав требования (интерфейсы), ты как раз и задал точки вариации для решения — то, как его можно расширять.
В ООП интерфейс является такой же точкой расширения, верно.
А теперь представь, что задача изменилась так, что недостаточно просто реализовать интерфейсы, чтобы получить решение.
В случае ООП, для решения новой задачи можно будет, как минимум, использовать классы из модели предметной области — базовый строительный материал.
А в случае ФП? Если декомпозиция на подзадачи велась исходя из деталей алгоритма решения конкретной задачи, нет никакой гарантии, что получившиеся подзадачи смогут пригодиться где-то ещё.
Вся разница в этом месте.
Проблема тут скорее теоретическая, чем практическая, т.к. на практике рулит кисс, сроки и чтобы работало
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>А теперь представь, что задача изменилась так, что недостаточно просто реализовать интерфейсы, чтобы получить решение. NGG>В случае ООП, для решения новой задачи можно будет, как минимум, использовать классы из модели предметной области — базовый строительный материал. NGG>А в случае ФП? Если декомпозиция на подзадачи велась исходя из деталей алгоритма решения конкретной задачи, нет никакой гарантии, что получившиеся подзадачи смогут пригодиться где-то ещё.
А в чем принципиальное различие? В случае ООП у нас есть классы, т.е. данные и функции для работы с ними. В случае ФП у нас есть то же самое — данные и функции для работы с ними. Иногда они объединены в одну синтаксическую структуру, например модуль, иногда просто описаны рядом. Как я понимаю, есть опасения, что в случае ФП функции для работы с данными не будут покрывать все возможные операции, а будут заточены на задачу. Но и в ООП случае гарантии универсальности описанных методов нет — для одной задачи данного набора методов оказалось достаточно, для другой могут потребоваться новые..
Здравствуйте, BulatZiganshin, Вы писали:
BZ>было:
BZ>D:\> Arc.exe create a enwik8 -m2 -t BZ>Compressed 1 file, 100.000.000 => 26.576.090 bytes. Ratio 26.5% BZ>Compression time 8.36 secs. Real time 8.92 secs, speed 11.212 kB/s BZ>Testing time 10.69 secs. Real time 11.19 secs, speed 8.940 kB/s
BZ>стало:
BZ>C:\> Arc.exe create a enwik8 -m2 -t BZ>Compressed 1 file, 100.000.000 => 26.576.090 bytes. Ratio 26.5% BZ>Compression time 9.22 secs. Real time 2.63 secs, speed 37.965 kB/s BZ>Testing time 14.12 secs. Real time 3.85 secs, speed 25.994 kB/s
Мне кажется, или Вы в итоге только проиграли за счет времени, проведенного в ядре (на блокировках, похоже)?
Здравствуйте, Mr.Cat, Вы писали:
MC>Мне кажется, или Вы в итоге только проиграли за счет времени, проведенного в ядре (на блокировках, похоже)?
Было-то 8.92, стало 2.63, как же проиграли? Прирост 3.4х. Не 4х, конечно, но всё же.
Здравствуйте, Mr.Cat, Вы писали:
BZ>>Compression time 8.36 secs. Real time 8.92 secs, speed 11.212 kB/s BZ>>стало: BZ>>Compression time 9.22 secs. Real time 2.63 secs, speed 37.965 kB/s
MC>Мне кажется, или Вы в итоге только проиграли за счет времени, проведенного в ядре (на блокировках, похоже)?
первая цифра — cpu time, время всех четырёх ядер. вторая — wall clock time, время от запуска программы до завершения. проигрыш в первом времени — обычное явление, результат нехватки пропускной способности памяти. грубо говоря, если бы память была бесконечно быстра, то первое время не возросло бы и мы бы смогли ускориться ровно вчетверо
Здравствуйте, D. Mon, Вы писали:
DM>А в чем принципиальное различие? В случае ООП у нас есть классы, т.е. данные и функции для работы с ними. В случае ФП у нас есть то же самое — данные и функции для работы с ними. Иногда они объединены в одну синтаксическую структуру, например модуль, иногда просто описаны рядом.
Разница в том, откуда берутся "данные" и "функции для работы с ними".
В ООП — из анализа предметной области (так говорит теория) => "классы" полученые для решения задачи А, останутся полезны для решения задачи Б из той же области (учитываем, что ооп применяется для создания систем, где основная сложность не алгоритмическая, а "размерная" — предметная область стоит из большого числа сущностей и отношений, а возникающие задачи относительно просты, но их много).
В ФП — как результат декомпопозиции _конкретной_ задачи на подзадачи (какими методами гуру пользуются в действительности — не знаю). Подзадачи же, в большой степени отражают алгоритм выбранный для решения исходной задачи, чем значимый аспект предметной области... (думаю, что гуру должны знать рецепты, как такого можно избежать, но статей на эту тему не видел).
DM> Как я понимаю, есть опасения, что в случае ФП функции для работы с данными не будут покрывать все возможные операции, а будут заточены на задачу. Но и в ООП случае гарантии универсальности описанных методов нет — для одной задачи данного набора методов оказалось достаточно, для другой могут потребоваться новые..
Дело не в том, что "могут потребоваться новые" (они потребуются при любом раскладе), а в том, насколько их реализация будет проста и как повлияет на систему в целом.
В случае ООП новые требования приводят к расширению и уточнению модели, а т.к. модель в значительной степени отражается 1-1 в "классы", новые требования не ведут к необходимости существенно изменять базу кода и не влияют на существующие решения... (до тех пор, пока изменения не коснулись самой предметной области или допущений сделанных относительно неё , но это другая песня).
В случае ФП — х3. На простых примерах всё выглядит здорово, кисс работает. Что будет если использовать фп там, где сейчас используется java — могу только гадать.
Пока считаю, что овчинка выделки не стоит, но надежд на лучшее не теряю Будь возможность выбора, по вполне очевидным причинам, предпочёл бы на замену java'е скалу, а не хаскелл, что дало бы (на вскидку) двухкратное сокращение затрат на кодирование (для тех задач, что мне приходится решать).
Здравствуйте, NotGonnaGetUs, Вы писали:
NGG>В ООП — из анализа предметной области (так говорит теория) => "классы" полученые для решения задачи А, останутся NGG>В ФП — как результат декомпопозиции _конкретной_ задачи на подзадачи (какими методами гуру пользуются в
Здравствуйте, BulatZiganshin, Вы писали: T>>>>4. Отфильтровать дерево, оставив только ветви содержащие дубликаты и сами дубликаты. T>>Как подправить, сходу не соображу. BZ>строй список дубликатов локально
Ага, разобрался. Красиво получается.
Возник ещё связанный вопрос:
При поиске дубликатов нужно сравнивать не полностью строку, а некоторую её часть.
Вроде бы ничего сложного — пишем функцию выделения существенной части строки (getIdent) и сипользуем именно её результат для поиска.
Строкиа имеет такой вид (prel):
/[\w\s]+(\([^)]+\)\s*)*/
Т.е. в начале несколько слов разделённых пробелами, и потом возможно несколько групп скобок.
Первый выриант getIdent был такой:
ltrim = dropWhile (==' ')
trim = reverse . ltrim . reverse . ltrim
getIdent = trim . takeWhile (/='(')
На моём объёме файла дерева ~8мб. выполнялось вполне шустро.
Теперь потребовалось включить в идентификатор первый символ из последней группы скобок, если она имеет такой вид (perl):
/\(\w,\s+[^)]+\)/
Т. е. getIdent стал такой:
getIdent str = first ++ getSmb other
where
first = trim $ takeWhile (/='(') str
other = last $ splitBy (=='(') str
getSmb (x:y:z:xs) | x == '(' && z == ',' = " (" ++ [y] ++ ")"
getSmb xs = ""
splitBy _ [] = []
splitBy f x = fist : splitBy f other
where
(fist, other) = break f x
И вот окончания его работы дождатся не получается.
Как ускорить этот кусок, и вообще как лучше работать с большими объёмами строк?
1. Есть большой текстовый файл A (несколько гигов, заведомо не помещается в память), нужно сделать текстовый файл A1, содержащий все строки из А, но отсортированные в алфавитном порядке. Ограничение по памяти — 256 МБ.
2. Изменить первую программу так, чтобы будучи запущена с ключом -u она создавала файл А2, содержащий все уникальные строки из А в алфавитном порядке и количество повторений для каждой строки. Т.е. из
dd ee ff
aaa bbb ccc
aaa bbb ccc
dd ee ff
aaa bbb ccc
Здравствуйте, Tonal-, Вы писали: T>И вот окончания его работы дождатся не получается.
Нашел ошибочку.
Функция splitBy циклилась.
На самом деле она идентична первоначальной groupFrom без проверки критерия для первого элемента.
getIdent str = first ++ (getSmb $ latest end)
where
first = strip $ takeWhile (/='(') str
end = groupFrom (=='(') str
latest [] = ""
latest xs = last xs
getSmb (x:y:z:xs) | x == '(' && z == ',' = " (" ++ [y] ++ ")"
getSmb xs = ""
T>... как лучше работать с большими объёмами строк?
Но этот вопрос таки остаётся.
Нужен поиск и замена подстроки.
Здравствуйте, Tonal-, Вы писали:
T>>... как лучше работать с большими объёмами строк? T>Но этот вопрос таки остаётся. T>Нужен поиск и замена подстроки.
Здравствуйте, D. Mon, Вы писали:
DM>Ограничение по памяти — 256 МБ.
это больше похоже не на практикум по *fp*, а на практикум по конкретным реализациям. интересоваться тем, как это делается в фп языках я бы рекомендовал только после того, как усвоен сам стиль фп программирования
Здравствуйте, MasterZiv, Вы писали:
>> ага. ты в курсе, что кол-во открытых файлов несколько ограничено?
MZ>Ага, а ты в курсе, что большинство предложенных решений построено MZ>по тому же принципу ? Я-то вот кэш открытых файлов сделал. MZ>Получилось много кода.
вообще-то я с самого начала подразумевал, что таких решений быть не должно. в фп языках не так уж сложно подготовить сначала данные в памяти чтобы затем писать по одному файлу
Здравствуйте, BulatZiganshin, Вы писали:
BZ>это больше похоже не на практикум по *fp*, а на практикум по конкретным реализациям.
Там фишка именно в дизайне — как обобщить сортировку на разные типы файлов с их нюансами. Я такую задачу решал на окамле с помощью модулей, но возможно есть более красивое решение.
BZ>интересоваться тем, как это делается в фп языках я бы рекомендовал только после того, как усвоен сам стиль фп программирования
Тогда прошу продемонстрировать этот самый стиль. Хотя бы костяк решения.
Здравствуйте, D. Mon, Вы писали:
DM>Там фишка именно в дизайне — как обобщить сортировку на разные типы файлов с их нюансами. Я такую задачу решал на окамле с помощью модулей, но возможно есть более красивое решение.
я может чего-то не понимаю, но о каком обобщении здесь идёт речь?
DM>Тогда прошу продемонстрировать этот самый стиль. Хотя бы костяк решения.
я как раз и говорю о том, что решение данной задачи не поможет развить фп-стиль программирования.
Здравствуйте, BulatZiganshin, Вы писали: T>>Для трёх буквочек можно нарисовать примерно так: BZ>ага. ты в курсе, что кол-во открытых файлов несколько ограничено?
Типа так:
#include <iostream>
#include <fstream>
#include <iterator>
#include <map>
#include <vector>
using namespace std;
typedef istream_iterator<string> word_iter_t;
typedef map<string, vector<string> > wmap_t;
typedef wmap_t::const_iterator wmap_iter_t;
typedef vector<string>::const_iterator witer_iter_t;
int main() {
wmap_t ofs;
ifstream words("words.txt");
for (word_iter_t cur(words), end; cur != end; ++cur) {
string fname = cur->substr(0, 3);
if (fname.size() > 0)
ofs[fname].push_back(*cur);
}
for (wmap_iter_t cur = ofs.begin(), end = ofs.end(); cur != end; ++cur) {
ofstream out(cur->first.c_str());
for (
witer_iter_t wcur = cur->second.begin(), end = cur->second.end();
wcur != end; ++wcur
)
out << *wcur << ' ';
}
}
Здравствуйте, D. Mon, Вы писали:
DM>1. Есть большой текстовый файл A (несколько гигов, заведомо не помещается в память), нужно сделать текстовый файл A1, содержащий все строки из А, но отсортированные в алфавитном порядке. Ограничение по памяти — 256 МБ.
import System.IO
import System.Directory
import Data.List
import Data.Maybe-- Разбиваем файл на куски определенного размера (заданного количеством строк)
-- Куски сортируем в памяти и записываем во временные файлы
-- Объединяем временные файлы в один отсортированный файл
sortBigFile srcFile dstFile maxSize = do
tempFiles <- splitFile maxSize srcFile
mergeChains tempFiles dstFile
mapM_ removeFile tempFiles -- удаляем мусор, который насоздавали
-- Возвращает ленивый список строк, содержащихся в файле
-- Позволяет работать со строками файла как со списком
-- В то же время не загружает полностью файл в памятьtype Stream = [String]
stream :: Handle -> IO Stream
stream handle = do
contents <- hGetContents handle
return $ lines contents
-- Делит файл на отсортированные куски размеров не больше maxSize строк
-- Функция возращает список временных файлов, содержащих отсортированные куски
splitFile :: Int -> FilePath -> IO [FilePath]
splitFile maxSize srcFile = do
h <- openFile srcFile ReadMode
s <- stream h
tempFiles <- splitFile' maxSize 0 s
hClose h
return tempFiles
-- Каждый кусок сортируется и записывается во временный файл
-- Имена временных файлов: temp0, temp1, и т.д.
-- xs - оставшиеся строки исходного файла, suffix - суффикс временного файла
splitFile' _ _ [] = return []
splitFile' maxSize suffix xs = do
let tempFile = "temp" ++ show suffix
let (chainLines, restLines) = splitAt maxSize xs
writeFile tempFile (unlines $ sort chainLines) -- сортировка и запись одного куска
restFiles <- splitFile' maxSize (suffix+1) restLines
return (tempFile : restFiles)
-- Объединяет отсортированные куски (fileNames) в один большой отсортированный файл dstFile
mergeChains :: [FilePath] -> FilePath -> IO ()
mergeChains fileNames dstFile = do-- dst <- dst' dstFile
dst <- openFile dstFile WriteMode
files <- mapM ((flip $ openFile) ReadMode) fileNames
strms <- mapM stream files
mergeChains' strms dst
mapM_ hClose files
hClose dst
-- Каждый объединяемый файл представлен потоком (ленивым списком) строк
-- Сравниваем первые строки потоков и минимальную строку отправляем в итоговый файл
-- streams - потоки строк временных файлов
-- dstHandle - итоговый файл
mergeChains' :: [Stream] -> Handle -> IO ()
mergeChains' streams dstHandle
| null strms = return ()
| otherwise = do hPutStrLn dstHandle minLine
mergeChains' (updateStreams strms) dstHandle
where
strms = filter (not.null) streams -- выкидываем обработанные потоки
minLine = minimum $ map head strms -- выбираем из всех потоков минимальную строку
-- В потоке, который содержал минимальную строку, переходим к следующим строкам
-- А остальные потоки оставляем без изменений
updateStreams (s:ss) | head s == minLine = (tail s) : ss
| otherwise = s : updateStreams ss
--------------
--- MAIN -----
-- main = sortBigFile "A" "A1" 10000
Не буду судить о пригодности этой задачи к практикуму ФП. Скажу лишь, что мне она добавила некоторых знаний.
Да и решить было просто интересно. Ну и ФП все же присутствует — вон тот же mergerChain' более-менее ФП-шный, плюс ленивость, работа с файлом как со списком и т.п.
Для тестов был создан файлик A и B, внутри которых — текст данной программы повторенный хз сколько раз.
Файл А — 11,5 Мб, 0,3 млн строк
Файл B — 91,5 Мб, 2,4 млн строк
В итоге:
обработка А — 30 временных файлов — 12 сек
обработка B — 240 временных файлов — 389 сек
Расход памяти для maxSize = 10000 строк по данным -sstderr около 15 Мб
splitFile — около 30% времени, mergerChains около 70%
В целом, около 50% времени занимает сравнение строк между собой.
Для сравнения попробовал запустить упрощенный вариант
main = do
content <- readFile "A"
writeFile "A1" (unlines $ sort $ lines content)
Для файла A — 7 сек, 313 Мб памяти
Для файла B — не получился результат
Сначала сказал, хочу больше виртуальной памяти. Добавил (оказалось была отключена, я даже и не знал работал целый год
Потом просто out of memory. Я так понял из-за ограничений на количество памяти для программ в Windows
Здравствуйте, Beam, Вы писали:
B>Не буду судить о пригодности этой задачи к практикуму ФП. Скажу лишь, что мне она добавила некоторых знаний.
если ограничивать число строк, а не объём занимаемой памяти — то нормально
main = do-- (Лениво) создаём отсортированные подсписки
sublists <- fmap sort1 getContents
-- Записываем их в файлы с именами 1..n
for (zip (map show [1..]) pairs) (uncurry writeFile)
-- Читаем (опять же лениво) содержимое всех ранее созданных файлов
sublists <- mapM (fmap lines readFile.show) [1..length sublists]
-- Сливаем их и записываем результат
putStr (unlines (mergesort' compare sublists)
sort1 = lines >>> unfoldr (\xs -> guard (xs>[]) >> splitAt 5000 xs) >>> map (sort>>>unlines)
-- Процедуру слияния упорядоченных списков можно найти в исходниках Data.List:
mergesort' :: (a -> a -> Ordering) -> [[a]] -> [a]
mergesort' cmp [] = []
mergesort' cmp [xs] = xs
mergesort' cmp xss = mergesort' cmp (merge_pairs cmp xss)
merge_pairs :: (a -> a -> Ordering) -> [[a]] -> [[a]]
merge_pairs cmp [] = []
merge_pairs cmp [xs] = [xs]
merge_pairs cmp (xs:ys:xss) = merge cmp xs ys : merge_pairs cmp xss
merge :: (a -> a -> Ordering) -> [a] -> [a] -> [a]
merge cmp xs [] = xs
merge cmp [] ys = ys
merge cmp (x:xs) (y:ys)
= case x `cmp` y of
GT -> y : merge cmp (x:xs) ys
_ -> x : merge cmp xs (y:ys)
на самом деле, даже если я ни в чём не ошибся — этот код будет держать все данные в памяти до победного. extra points — за изящное решение этой маааленькой проблемы
Здравствуйте, BulatZiganshin, Вы писали:
BZ>если ограничивать число строк, а не объём занимаемой памяти — то нормально BZ>
BZ>main = do-- (Лениво) создаём отсортированные подсписки
BZ> sublists <- fmap sort1 getContents
BZ> -- Записываем их в файлы с именами 1..n
BZ> for (zip (map show [1..]) pairs) (uncurry writeFile)
BZ> -- Читаем (опять же лениво) содержимое всех ранее созданных файлов
BZ> sublists <- mapM (fmap lines readFile.show) [1..length sublists]
BZ> -- Сливаем их и записываем результат
BZ> putStr (unlines (mergesort' compare sublists)
BZ>sort1 = lines >>> unfoldr (\xs -> guard (xs>[]) >> splitAt 5000 xs) >>> map (sort>>>unlines)
BZ>
1. Так и не смог я это запустить. В принципе идея ясна, но хочется увидеть решение, которе хотя бы компилируется (в т.ч. импорт)
А в идеале — полный пример, с открытием, закрытием файлов и пр.
2. Я думаю ленивость здесь теряется на readFile, т.к. файл закрывается и все данные попадают в память.
Здравствуйте, Beam, Вы писали:
B>1. Так и не смог я это запустить. В принципе идея ясна, но хочется увидеть решение, которе хотя бы компилируется (в т.ч. импорт)
ну симпортируйте стандартные Data.List, Control.Monad и прочая
B>А в идеале — полный пример, с открытием, закрытием файлов и пр.
это полный пример
B>2. Я думаю ленивость здесь теряется на readFile, т.к. файл закрывается и все данные попадают в память.
нет. readFile = openFile >>= hGetContents, т.е. он читает файл так же лениво и безалаберно
Здравствуйте, BulatZiganshin, Вы писали:
BZ>я может чего-то не понимаю, но о каком обобщении здесь идёт речь?
Обобщение алгоритма сортировки на две подзадачи: в одном случае просто сортировка, в другом — с оставлением только уникальных строк и подсчетом их количества. Например, там могут быть разные функции чтения, записи, сравнения и комбинирования, а функция сортировки — одна общая.
BZ>я как раз и говорю о том, что решение данной задачи не поможет развить фп-стиль программирования.
Тут немного ранее обсуждали, что на слишком маленьких задачах не продемонстрировать ФП-подход к дизайну, чтобы можно было его сравнить с ООП-дизайном. Мне показалось, что данная задача уже дает такую возможность.
Здравствуйте, D. Mon, Вы писали:
BZ>>я может чего-то не понимаю, но о каком обобщении здесь идёт речь?
DM>Обобщение алгоритма сортировки на две подзадачи: в одном случае просто сортировка, в другом — с оставлением только уникальных строк и подсчетом их количества. Например, там могут быть разные функции чтения, записи, сравнения и комбинирования, а функция сортировки — одна общая.
можно конечно, но по-моему это слишком сложно. проще поставить агрегатор после merge
BZ>>я как раз и говорю о том, что решение данной задачи не поможет развить фп-стиль программирования.
DM>Тут немного ранее обсуждали, что на слишком маленьких задачах не продемонстрировать ФП-подход к дизайну, чтобы можно было его сравнить с ООП-дизайном. Мне показалось, что данная задача уже дает такую возможность.
проблема на самом деле в том, что фп дизайн по опыту программирования в яве понять так же невозможно, как ооп дизайн по опыту ассемблера
кроме того, обсуждать его надо не на примере полного решения маленьких задач, а на разборе именно дизайна крупных задач или библиотек
Здравствуйте, D. Mon, Вы писали:
DM>Здравствуйте, BulatZiganshin, Вы писали:
BZ>>я может чего-то не понимаю, но о каком обобщении здесь идёт речь?
DM>Обобщение алгоритма сортировки на две подзадачи: в одном случае просто сортировка, в другом — с оставлением только уникальных строк и подсчетом их количества. Например, там могут быть разные функции чтения, записи, сравнения и комбинирования, а функция сортировки — одна общая.
Неплохо, только на файле из 20 млн строк получится нужно одновременно открыть 2000 файлов. ОС позволит? Попарный мердж, как у Булата, тут подойдет лучше.
А теперь самое интересное — пункт 2 (про подсчет уникальных строк). Интересно, как сильно надо будет менять программу для его реализации.
Здравствуйте, D. Mon, Вы писали:
B>>mergeChains :: [FilePath] -> FilePath -> IO () DM>Неплохо, только на файле из 20 млн строк получится нужно одновременно открыть 2000 файлов. ОС позволит? Попарный мердж, как у Булата, тут подойдет лучше.
Ну, все не так плохо. Там же задается maxSize.
Вот расход памяти для разных maxSize, применительно к тестовым файлам.
maxSize = 20000 — 29 Mb
maxSize = 50000 — 61 Mb
maxSize = 200000 — 273 Mb
т.е. используя maxSize = 200 тыс., файл 20 млн. строк даст 10 файлов
Хотя, конечно, когда-нибудь упремся в количество файлов
Попарное слияние увеличивает время работы алгоритма.
Если мы держим только несколько файлов открытыми (не все), то и слияние в данный момент можем сделать только на них. Мы их читаем, потом объединяем в один файл побольше. Но этот файл (который побольше) снова надо объединять с другими, а значит мы его снова должны прочитать. В любом случае, если делать серьезную реализацию, надо учитывать возможность ограничения на количество открытых файлов, а соответственно — делать промежуточное слияние.
Алгоритм Булата практически ничем не отличается. Только он красивее выглядит Алгоритм слияния хоть и "выглядит попарным" (так проще), на самом деле для его работы также открываются все файлы одновременно.
К тому же, на текущий момент, там все данные загружаются в память, а значит вообще временные файлы не нужны.
Надежда была на полную ленивость, но программа получается энергичная. Хотелось бы это исправить.
DM>А теперь самое интересное — пункт 2 (про подсчет уникальных строк). Интересно, как сильно надо будет менять программу для его реализации.
Позже попробую написать. Но принцип, думаю, будет такой:
В mergeChains', там где строка выводится в итоговый файл добавим функцию, которая будет подсчитывать количество повторов последней строки, а затем выводить ее саму и количество в другой файл A2.
Здравствуйте, BulatZiganshin, Вы писали:
BZ>вот пример. представим классическую gui-библиотеку.
Кстати, да. Представим. Как с помощью ФП представлять такие абстрации, как органы управления вроде button, editbox, listbox и пр.? Т.е. как их с нуля реализовать пользуясь только ФП?
Это не сарказм, не подколка. Просто я пока не представляю себе этого.
SObjectizer: <микро>Агентно-ориентированное программирование на C++.
Здравствуйте, eao197, Вы писали:
BZ>>вот пример. представим классическую gui-библиотеку.
E>Кстати, да. Представим. Как с помощью ФП представлять такие абстрации, как органы управления вроде button, editbox, listbox и пр.? Т.е. как их с нуля реализовать пользуясь только ФП?
да так же, как и в любом другом процедурном языке. у вас от 20-летия ООП крыши совсем посносило. вы уже верите, что до ооп не было ни библиотек, ни повторного использования кода
Здравствуйте, BulatZiganshin, Вы писали:
E>>Кстати, да. Представим. Как с помощью ФП представлять такие абстрации, как органы управления вроде button, editbox, listbox и пр.? Т.е. как их с нуля реализовать пользуясь только ФП?
BZ>да так же, как и в любом другом процедурном языке. у вас от 20-летия ООП крыши совсем посносило. вы уже верите, что до ооп не было ни библиотек, ни повторного использования кода
Извини, это общие слова. Даже когда писались окна на процедурных языках, все равно там была какая-то эмуляция ООП. И были проблемы с наследованием и переопределением поведения.
А как в ФП с функциями и отсутствием изменяемого состояния все это сделать нормально?
SObjectizer: <микро>Агентно-ориентированное программирование на C++.
ещё одна задача из моей практики. есть список файлов, каждый файл представлен структурой, содержащей поля ext (расширение файла), size (его размер) и другие, нам неинтересные. нужно разбить этот список на группы (солид-блоки) по критериям, описанным строкой вида [Nf][Mb][e], где N и M — некоторые числа. эта запись означает, что каждая группа может содержать не более N файлов, суммарным объёмом не более M байт, с одинаковым расширением. все три части этой строки необязательны — при их отсутствии соответствующий критерий не применяется
Здравствуйте, BulatZiganshin, Вы писали: BZ>ещё одна задача из моей практики.
Есть ли дополнительные ограничения на результирующие группы? Например, идет ли речь о разбиении на минимальное число групп? Или критерий оценки решения — элегантность и разумная вычислительная сложность?
Здравствуйте, Mr.Cat, Вы писали:
MC>Здравствуйте, BulatZiganshin, Вы писали: BZ>>ещё одна задача из моей практики.
MC>Есть ли дополнительные ограничения на результирующие группы? Например, идет ли речь о разбиении на минимальное число групп? Или критерий оценки решения — элегантность и разумная вычислительная сложность?
при разбиении нельзя менять порядок файлов. проводится оно лишь приблизительно (в смысле суммарного объёма файлов в каждой группе)
Здравствуйте, BulatZiganshin, Вы писали:
BZ>я часто сталкиваюсь с двумя точками зрения — первая "я знаком с ФП, но оно никакой выгоды в программировании не даёт", и вторая "я знаком с ФП, но так и не научился использовать его для повышения эффективности программирования". собственно, это одно и то же, разница только в том, на кого (ФП или самого человека) возлагается вина за отсутствие результата
Если ты часто сталкиваешся с подобным мнением, то тебе не составит труда дать ссылки на 5-6 примеров такого мнения.
Дай, плиз.
BZ>1) есть список слов. надо записать в файл "a" все слова, начинающиеся на букву 'a', в файл "b" — слова на 'b' и т.д. вплоть до z
Что-то я не пойму... Ты вроде говорил о задачах из реальной жизни, а предлагаешь написать какие-то примитивные примерчики краткость и понятность которых в основном зависят от наличия удобных библиотек.
Вот решение твоей задачи на Немерле в императивной монере:
def words = ["abba", "yield"];
foreach (group in words.GroupBy(w => w[0]))
File.WriteAllText($"$(group.Key).txt", $"..$group");
Что тут из реального мира?
BZ>2) имеется список имён файлов в архиве:
BZ>dir1\file1 BZ>dir1\dir2\file2 BZ>dir1\dir3\file3 BZ>dir4\file4
BZ>требуется написать функцию, которая по имени каталога возвращает список файлов и подкаталогов в нём, к примеру
BZ>f "dir1" = (["file1"], ["dir2","dir3"])
BZ>a) сложностью O(кол-во файлов) BZ>б) более эффективную
Здравствуйте, VoidEx, Вы писали:
VD>>Ты не согласен с тем, что задачки детские и не имеющие ничего общего с реальным миром?
VE>Ну ты ж решение не привёл, значит не решил детскую задачу
Вообще-то привел, хотя и не обязан был.
VE>Точнее, решение-то привёл, но своей какой-то задачи, а не поставленной.
Я привел решение задачи так как ее понял. То что она не определена строго формально — это не моя вина.
ЗЫ
В общем, опять не по делу говоришь.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
VD>Я привел решение задачи так как ее понял. То что она не определена строго формально — это не моя вина.
Что? Перечитай условие ещё раз. Дан список имён файлов. Какое именно слово тебе не ясно?
Откуда взялась какая-то директория rootDir? Куда передать исходный список имён файлов и откуда получить результат?
Где решение-то?
VD>Вот решение твоей задачи на Немерле в императивной монере:
VD>def words = ["abba", "yield"];
VD>foreach (group in words.GroupBy(w => w[0]))
VD> File.WriteAllText($"$(group.Key).txt", $"..$group");
Можно вопрос. Так это императивная монера, или нет?
В своей последней статье это у тебя функциональный подход.
Вы бы уже определились что ли?
Итого:
1-я задача решена кратко в функциональном стиле
2-я задача не решена вообще
Здравствуйте, VladD2, Вы писали:
VD>Если ты часто сталкиваешся с подобным мнением, то тебе не составит труда дать ссылки на 5-6 примеров такого мнения. VD>Дай, плиз.
VD>Вот решение твоей задачи на Немерле в императивной монере:
тут интересно вспомнить, что это решение тебе не пришло в голову до тех пор, пока я не привёл его на хаскеле. т.е. это как раз пример того, как фп подход помог упростить решение задачи
BZ>>2) имеется список имён файлов в архиве:
BZ>>dir1\file1 BZ>>dir1\dir2\file2 BZ>>dir1\dir3\file3 BZ>>dir4\file4
BZ>>требуется написать функцию, которая по имени каталога возвращает список файлов и подкаталогов в нём, к примеру
BZ>>f "dir1" = (["file1"], ["dir2","dir3"])
BZ>>a) сложностью O(кол-во файлов) BZ>>б) более эффективную
ты невнимательно прочёл. у нас есть список файлов, содержащихся В АРХИВЕ. и нужна функция, возвращающая спсиок файлов/каталогов определённом каталоге внутри архива. причём в двух вариантах — простом и с построением промежуточной структуры данных для ускорения поиска
Здравствуйте, VoidEx, Вы писали:
VE>Что? Перечитай условие ещё раз. Дан список имён файлов. Какое именно слово тебе не ясно? VE>Откуда взялась какая-то директория rootDir? Куда передать исходный список имён файлов и откуда получить результат? VE>Где решение-то?
имеется список имён файлов в архиве...требуется написать функцию, которая по имени каталога возвращает список файлов и подкаталогов в нём, к примеру...
Я так понял речь идет о каталоге. Ну, даже если я понял не верно, все равно задача от это реалистичной и темболе что-то демонстрирующей не становится. Проектирования в ней — 0. Банальная мелкая функция. Опять же количество вопросов только увеличивается. Как, например, понять что что-то файл, а что-то каталог?
VD>>Вот решение твоей задачи на Немерле в императивной монере: VE>
VD>>def words = ["abba", "yield"];
VD>>foreach (group in words.GroupBy(w => w[0]))
VD>> File.WriteAllText($"$(group.Key).txt", $"..$group");
VE>
VE>Можно вопрос. Так это императивная монера, или нет?
Цикл видишь? С каких пор в ФП циклы стали. GroupBy — конечно функция. Причем функция высшего порядка. Так что конечно подход можно назвать смешанным, но он точно не чисто функциональный. И таким, смешанным способом задачу решить куда проще чем выпендриваясь в функциональном стиле. Запись в файл сама по себе является императивной операцией. Просто глупо пытаться осуществить ее с помощью функциональных изысков.
формирование же строк производится вообще макросами. Любой хаскель нерно курит по сравнению с ними, так как при большем объеме шума и рядом не будет стоять по скорости.
Итого пример скорее демонстрирует бессмысленность функционального подхода для выполнения императивных операций. И одновременно удобство его же при обработке списков.
Все чему такой пример может научить, так это смотреть на некоторые вещи как на обработку списков. Но стоило ли такую банальную мысль облачать в красивое слово "мастер-класс".
VE>В своей последней статье это у тебя функциональный подход. VE>Вы бы уже определились что ли?
См. выше.
VE>Итого: VE>1-я задача решена кратко в функциональном стиле VE>2-я задача не решена вообще
Если честно, мне вообще их решать было не нужно. В прочем, ты мягко говоря лукавишь.
Еще раз. Основной смысл моих слов заключался в том, что такие примеры ничего ровным счетом не демонстрируют.
Основные проблемы ФП заключаются в том, что чистый ФП не предоставляют средств проектировани более-менее серьезных систем. И если уж и устраивать мастер-классы, то неплохо было бы продемонстрировать новичкам как с помощью одного лишь ФП спроектировать и написать каркас небольшого, но не примитивного приложения.
С удовольствием бы послушал тех кто считает, что он в силах это сделать. Лично я не вижу как это сделать. Я бы воспользовался бы ООП для проектирования каркаса и ФП для реализации его частей.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Кстати, ты полез обсуждать последний пример, но пропустил мой первый вопрос.
А мне вот интересно согласен ли ты с автором темы по поводу:
я часто сталкиваюсь с двумя точками зрения — первая "я знаком с ФП, но оно никакой выгоды в программировании не даёт", и вторая "я знаком с ФП, но так и не научился использовать его для повышения эффективности программирования". собственно, это одно и то же, разница только в том, на кого (ФП или самого человека) возлагается вина за отсутствие результата
Я вот с ними ни разу не сталкивался. Я сталкивался с непониманием того, что такое ФП. С заявлениями, что "у меня и так все хорошо и мне хватает Блаба". И с заявлениями о том, что ФП человек не понимает и не хочет или не может его понять.
А так чтобы человек говорил, что понимает ФП но не знает как его применить лично я не сталкивался.
Так вот часто ли ты слышал вопросы в постановке автора темы?
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, BulatZiganshin, Вы писали:
BZ>Здравствуйте, VladD2, Вы писали:
VD>>Если ты часто сталкиваешся с подобным мнением, то тебе не составит труда дать ссылки на 5-6 примеров такого мнения. VD>>Дай, плиз.
BZ>а почему не 500-600?
Дай 600, если сможешь. В общем-то любое количество большее двух уже устроило бы. А то я то и одного такого заявления не слышал. Сдается мне, что кто-то хочет побороться с ветряными мельницами.
BZ>вот к примеру: http://rsdn.ru/Forum/Default.aspx?mid=3254353
Дурака из себя строишь?
В прочем, я так и думал, что ссылок у тебя нет. Видимо по тому, что их нет в природе и ты действительно нашел себе мельницу чтобы по воевать.
VD>>Вот решение твоей задачи на Немерле в императивной монере:
BZ>тут интересно вспомнить, что это решение тебе не пришло в голову до тех пор, пока я не привёл его на хаскеле. т.е. это как раз пример того, как фп подход помог упростить решение задачи
Я вообще не читал ветки вглубь. Да и скромнее надо быть.
К тому же не помню, чтобы в хастелое были подобные функции группировки по ключу.
Что же до упрощения, то конечно когда речь идет о задачах преобразования последовательностей, то ФП рулит. Он ведь родом из Лиспа. А у Лиспа название говорит само за себя.
Вот только ты ведь наверно заметил, что половина решения использует ФП, а вторая старый добрый императивный подход который в данном случае куда удобнее ФП. Ведь задача записи в файл императивна по сути.
Так что пример отлично демонстрирует, что один ФП жив не будешь. И что нужно грамотно выбирать что нужно делать в функциональном стиле, а что в императивном.
Но претензия тут у меня в другом. На мой взгляд задачи слишком малы чтобы говорить о проектировании. А именно это не понятно большинству императивных программистов столкнувшихся с ФП.
BZ>ты невнимательно прочёл. у нас есть список файлов, содержащихся В АРХИВЕ. и нужна функция, возвращающая спсиок файлов/каталогов определённом каталоге внутри архива. причём в двух вариантах — простом и с построением промежуточной структуры данных для ускорения поиска
ОК. Мне тут уже намекнули на это. Согласен. Наверно я не верно понял слово "архив". Почему-то подумал, что ты назвал так каталог.
Но претензия к задаче все та же. Она примитивна. Это тактические задачи. На них нельзя продемонстрировать приемущества ФП. В любом случае задача решится написанием более-менее приемлемой промежуточной абстракции (в том или ином виде). Тут подойдут и императивные итераторы, и функциональная рекурсия.
BZ>далее по тексту можно найти ещё три задачи
Не интересны они. Это упражнения на тему "как бы я реализовал ту или иную утилитарную функцию".
Я для себя уже давно понял, что ФП сам по себе панацеей не является. Это отличный взгляд декомпозицию алгоритмов. Но кроме алгоритмов в современных программах есть еще кое что — архитектура. И это кое что ФП упростить не в силах. Вот если бы ты показал примеры где ФП давало бы архитектурные преимущества, то я бы лично поблагодарил тебя от всей души.
Пока что ФП даем мне следующие приемущества:
1. Декомпозиция на микроскопическом уровне. Это позволяет то, что я раньше писал циклами (от безысходности) поместить в маленькие функции принимающие "уточняющие куски кода" (т.е. в Фвп).
2. Алгебраические типы данных позволяют упростить задачи анализа и/или преобрзования иерархий объектов.
3. Неизменяемость позволяет упростить отладку и распаралеливание/параллельный доступ.
Все это я бы назвал тактическими преимуществами. Они очень приятны и важны. Но для полного превосходства нужно иметь стратегические преимущества — упрощение дизайна приложений. Если же стратегических приемуществ достичь нельзя, то надо совмещать ФП, с чем-то еще. На сегодня это ООП, КОП и МП.
Если я не прав, то хотелось бы услышать развернутое опровержение. Только плиз, без догм.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
VD>Цикл видишь? С каких пор в ФП циклы стали. GroupBy — конечно функция. Причем функция высшего порядка. Так что конечно подход можно назвать смешанным, но он точно не чисто функциональный. И таким, смешанным способом задачу решить куда проще чем выпендриваясь в функциональном стиле. Запись в файл сама по себе является императивной операцией. Просто глупо пытаться осуществить ее с помощью функциональных изысков.
Хм, чем foreach, называемый тобой в данном случае циклом, проще или сложнее map(M)?
mapM_ writeToFile lst
Это что, императивно? А между прочим, пишет в файл.
VD>формирование же строк производится вообще макросами. Любой хаскель нерно курит по сравнению с ними, так как при большем объеме шума и рядом не будет стоять по скорости.
Это просто хвастовство, или какое-то отношение к теме имеет?
VE>>В своей последней статье это у тебя функциональный подход. VE>>Вы бы уже определились что ли?
VD>См. выше.
Там пространные какие-то рассуждения. Ты скажи одним словом, функциональный это подход или императивный?
А то у тебя от контекста зависит по ходу. Теперь вот это вообще смешанный подход оказался.
Здравствуйте, VladD2, Вы писали:
VD>Кстати, ты полез обсуждать последний пример, но пропустил мой первый вопрос. VD>А мне вот интересно согласен ли ты с автором темы по поводу: VD>
VD>я часто сталкиваюсь с двумя точками зрения — первая "я знаком с ФП, но оно никакой выгоды в программировании не даёт", и вторая "я знаком с ФП, но так и не научился использовать его для повышения эффективности программирования". собственно, это одно и то же, разница только в том, на кого (ФП или самого человека) возлагается вина за отсутствие результата
Не размышлял на эту тему, так что не могу сказать, согласен или нет. Но точка зрения такая зачастую присутствует, хотя это не относится только к ФП. Это вообще нередкость — думать, что "знакомы с (подставь нужное), но никакой выгоды нет".
VD>Я вот с ними ни разу не сталкивался. Я сталкивался с непониманием того, что такое ФП. С заявлениями, что "у меня и так все хорошо и мне хватает Блаба". И с заявлениями о том, что ФП человек не понимает и не хочет или не может его понять.
Куда хуже, когда человек думает, что всё понял.
VD>А так чтобы человек говорил, что понимает ФП но не знает как его применить лично я не сталкивался.
Не сочти за оскорбление, но
Лично я не вижу как это сделать. Я бы воспользовался бы ООП для проектирования каркаса и ФП для реализации его частей.
VD>Так вот часто ли ты слышал вопросы в постановке автора темы?
Нет
Здравствуйте, VladD2, Вы писали:
VD>Так что пример отлично демонстрирует, что один ФП жив не будешь. И что нужно грамотно выбирать что нужно делать в функциональном стиле, а что в императивном.
Задача исходного поста была показать, как функциональный подход может дать преимущества.
приводил решение в функциональном стиле, которое элементарно параллелится. В императивном предлагалось менять какие-то общие переменные, отсюда все вытекающие.
Ты, конечно, можешь взять queue и назвать всё в итоге императивщиной, но это, очевидно, не так.
P.S. Хочу попросить тебя о маленьком одолжении: не приплетать макросы (которые только поднимут очередной холивар, в котором ты обвинишь окружающих) и не извращать слова остальных участников (а то уже началось, будто ФП предлагают панацеей, хотя нигде этого не сказано). Было бы чудно.
P.P.S. Почему ссылка и последующее слово сливаются? Там точно есть пробел
Здравствуйте, VladD2, Вы писали:
VD>>>Вот решение твоей задачи на Немерле в императивной монере: VE>>
VD>>>def words = ["abba", "yield"];
VD>>>foreach (group in words.GroupBy(w => w[0]))
VD>>> File.WriteAllText($"$(group.Key).txt", $"..$group");
VE>>
VE>>Можно вопрос. Так это императивная монера, или нет?
VD>Цикл видишь? С каких пор в ФП циклы стали.
Если придраться к терминам, то это не совсем цикл, а внутренний итератор, реализированный как конструкция языка... А это скорее ФП чем не ФП
<< RSDN@Home 1.2.0 alpha 4 rev. 1128>>
Сейчас играет In Extremo — Tannhuser
Здравствуйте, Gajdalager, Вы писали:
G>Если придраться к терминам, то это не совсем цикл, а внутренний итератор, реализированный как конструкция языка... А это скорее ФП чем не ФП
Ммм, разве итераторы стали функциональными?
Вон на хаскеле их чтот ни разу не видел, наверное он нефункциональный?
Здравствуйте, Курилка, Вы писали:
К>Ммм, разве итераторы стали функциональными? К>Вон на хаскеле их чтот ни разу не видел, наверное он нефункциональный?
Как это, а вот это — [a] — что?
На императивных ленивые списки (потоки, последовательности, етц.) эмулируется итераторами, так что это очень даже функциональная фишка.
Здравствуйте, VladD2, Вы писали: VD>Все это я бы назвал тактическими преимуществами. Они очень приятны и важны. Но для полного превосходства нужно иметь стратегические преимущества — упрощение дизайна приложений.
А на мой взгляд упрощение дизайна в некоторых случаях достигается. Мне вот тут на днях надо было простенький веб-интерфейс сделать, и я решил сделать его на scheme. PLT-шный фреймворк показался удачным — и когда я решил узнать про него побольше, оказалось, это т.н. основанный на продолжениях (continuations based) веб-фреймворк. Фишка таких фреймворков в том, что вместо манипулирования состоянием, программист манипулирует континуацией. Т.е., например, отправляя юзеру форму, программист указывает функцию, которая получит данные формы. Получается вполне удобно.
Здравствуйте, Курилка, Вы писали:
К>Здравствуйте, Gajdalager, Вы писали:
G>>Если придраться к терминам, то это не совсем цикл, а внутренний итератор, реализированный как конструкция языка... А это скорее ФП чем не ФП
К>Ммм, разве итераторы стали функциональными? К>Вон на хаскеле их чтот ни разу не видел, наверное он нефункциональный?
ИМХО внутренний итератор как раз привносит функциональную парадигму, т.к. являет собой использование ФВП (в не-ФП языках эмулированных с помощью паттерна Command). По духу вполне функционально. И ф-я foreach — разве это не внутренний итератор (если использовать терминологию GoF)?
Ну а внешний итератор, то который next/hasNext функциональным вряд ли можно назвать, конечно
<< RSDN@Home 1.2.0 alpha 4 rev. 1128>>
Сейчас играет In Extremo — Singapur
VE>>Это что, императивно? А между прочим, пишет в файл.
Т>Раз пишет в файл, значит императивно. По определению.
Согласен. А здесь — у нас только функция evalIO_ императивна, или writeAll тоже?
{-# LANGUAGE GADTs #-}data IO_ a where
WriteToFile :: String -> IO_ ()
Sequence :: [IO_ ()] -> IO_ ()
-- etc
writeAll xs = Sequence (map WriteToFile xs)
evalIO_ :: IO_ a -> IO a
evalIO_ = ...
Вообще спор старый как мир, и по большому счету бессмысленный. Разница между императивным и декларативным — в голове наблюдателя. А наличие какой-то системы эффектов (одним из вариантов реализации которой являются монады — заметьте, я не утверждаю, что самым лучшим) — объективный факт, все остальное — спекуляции.
Здравствуйте, Трурль, Вы писали: Т>Раз пишет в файл, значит императивно. По определению.
А вот и нет. На вход мы получаем пустой файл, на выходе получаем новый — с данными. То, что гадкая императивная ОСь записывает новенький файл на место старого — уже не наша забота.
Здравствуйте, Mr.Cat, Вы писали:
MC>А вот и нет. На вход мы получаем пустой файл, на выходе получаем новый — с данными. То, что гадкая императивная ОСь записывает новенький файл на место старого — уже не наша забота.
Запись файла — это наблюдаемый побочный эффект, причем наблюдаемый не только из гадкой императивной оси, но из нашей непорочно чистой программы. Повторюсь, весь фокус — в типизации эффектов.
Здравствуйте, palm mute, Вы писали:
PM>Здравствуйте, Mr.Cat, Вы писали:
MC>>А вот и нет. На вход мы получаем пустой файл, на выходе получаем новый — с данными. То, что гадкая императивная ОСь записывает новенький файл на место старого — уже не наша забота.
PM>Запись файла — это наблюдаемый побочный эффект, причем наблюдаемый не только из гадкой императивной оси, но из нашей непорочно чистой программы. Повторюсь, весь фокус — в типизации эффектов.
Осторожно, сейчас вы опять с темы съедете Речь шла не о pure-functional и не о отсутствии side-effects, а о программировании в функциональной монере (с). Манера программирования всё же далека от теоретических изысков и подразумевает преимущественное использование неких практик — к ним чаще всего относят использование ФВП, замыкания, иммутабельные структуры и отсутствие side-effects. Однако это не значит, что если в программе есть хоть один side-effect, то она императивна. Опять таки, это спор о терминах, но!
def words = ["abba", "yield"];
foreach (group in words.GroupBy(w => w[0]))
File.WriteAllText($"$(group.Key).txt", $"..$group");
Я бы назвал эту программу скорее функциональной чем императивной. Есть у меня чёткое ощущение, что внутренний итератор был заимствован из функционального мира.
<< RSDN@Home 1.2.0 alpha 4 rev. 1128>>
Сейчас играет In Extremo — Mein Kind
Здравствуйте, Gajdalager, Вы писали:
G>Я бы назвал эту программу скорее функциональной чем императивной. Есть у меня чёткое ощущение, что внутренний итератор был заимствован из функционального мира.
Подобные споры возникают с завидной регулярностью именно из-за того, что ощущения у всех разные, и разница между императивным и декларативным находится в философской плоскости. А оффтопик в этой ветке начался значительно раньше моего сообщения.
Переделал я свой код. Вот что получилось.
Теперь у нас есть bigSort, которая не привязана к файлам и может сортировать большие списки (в условиях ограничений памяти).
a |> f = f a
-- сортирует длинный список
-- разбивает список на последовательные блоки, сортирует их, записывает во временные файлы, затем файлы читает и объединяет
bigSort :: (Read a, Ord a, Show a) => [a] -> IO [a]
bigSort xs = xs |> breakUp 10000 |> map sort |> writeChains >>= readChains >>= (merge >>> return)
-- сохраняет блоки во временные файлы, возвращает имена файлов, куда записал
writeChains chains = do
let tempFiles = [1..] |> map show |> map ("temp"++)
let writeChain' (filename, xs) = xs |> map show |> unlines |> writeFile filename >> return filename
mapM writeChain' (zip tempFiles chains)
-- читает блоки из временных файлов
readChains files = do
let readChain' file = readFile file >>= (lines >>> map read >>> return)
mapM readChain' files
-- разбивает список на части размером size
breakUp :: Int -> [a] -> [[a]]
breakUp size xs = let takeBlock ps = guard (null ps |> not) >> Just (splitAt size ps)
in unfoldr takeBlock xs
-- объединяет несколько упорядоченных списков в один упорядоченный список
merge :: (Ord a) => [[a]] -> [a]
merge [] = []
merge ss = foldl1 mergePair ss
mergePair [] ss = ss
mergePair ss [] = ss
mergePair (s1:ss1) (s2:ss2) | s1 <= s2 = s1 : mergePair ss1 (s2:ss2)
| otherwise = s2 : mergePair (s1:ss1) ss2
DM>1. Есть большой текстовый файл A (несколько гигов, заведомо не помещается в память), нужно сделать текстовый файл A1, содержащий все строки из А, но отсортированные в алфавитном порядке. Ограничение по памяти — 256 МБ.
main = do
args <- getArgs
let src = head args
let dst = src ++ "_sorted"
readFile src >>= (lines >>> bigSort) >>= (unlines >>> writeFile dst)
DM>2. Изменить первую программу так, чтобы будучи запущена с ключом -u она создавала файл А2, содержащий все уникальные строки из А в алфавитном порядке и количество повторений для каждой строки.
main2 = do
args <- getArgs
let src = head args
let dst = src ++ "_sorted"let dup = src ++ "_dup"let needDup = any (=="-u") args
sortedData <- readFile src >>= (lines >>> bigSort)
hdst <- openFile dst WriteMode
hdup <- openFile dup WriteMode
-- количество одинаковых строк и сами эти строкиlet gs = [(length g, head g) | g <- group sortedData]
-- пишем параллельно в sorted и duplet writeGroup (count, str) = do
replicateM_ count (hPutStrLn hdst str)
if needDup then hPutStrLn hdup (show count ++ ": " ++ str) else return ()
mapM_ writeGroup [(length g, head g) | g <- group sortedData]
hClose hdup
hClose hdst
Переделал я свой код. Вот что получилось.
Теперь у нас есть bigSort, которая не привязана к файлам и может сортировать большие списки (в условиях ограничений памяти).
a |> f = f a
-- сортирует длинный список
-- разбивает список на последовательные блоки, сортирует их, записывает во временные файлы, затем файлы читает и объединяет
bigSort :: (Read a, Ord a, Show a) => [a] -> IO [a]
bigSort xs = xs |> breakUp 10000 |> map sort |> writeChains >>= readChains >>= (merge >>> return)
-- сохраняет блоки во временные файлы, возвращает имена файлов, куда записал
writeChains chains = do
let tempFiles = [1..] |> map show |> map ("temp"++)
let writeChain' (filename, xs) = xs |> map show |> unlines |> writeFile filename >> return filename
mapM writeChain' (zip tempFiles chains)
-- читает блоки из временных файлов
readChains files = do
let readChain' file = readFile file >>= (lines >>> map read >>> return)
mapM readChain' files
-- разбивает список на части размером size
breakUp :: Int -> [a] -> [[a]]
breakUp size xs = let takeBlock ps = guard (null ps |> not) >> Just (splitAt size ps)
in unfoldr takeBlock xs
-- объединяет несколько упорядоченных списков в один упорядоченный список
merge :: (Ord a) => [[a]] -> [a]
merge [] = []
merge ss = foldl1 mergePair ss
mergePair [] ss = ss
mergePair ss [] = ss
mergePair (s1:ss1) (s2:ss2) | s1 <= s2 = s1 : mergePair ss1 (s2:ss2)
| otherwise = s2 : mergePair (s1:ss1) ss2
DM>1. Есть большой текстовый файл A (несколько гигов, заведомо не помещается в память), нужно сделать текстовый файл A1, содержащий все строки из А, но отсортированные в алфавитном порядке. Ограничение по памяти — 256 МБ.
main = do
args <- getArgs
let src = head args
let dst = src ++ "_sorted"
readFile src >>= (lines >>> bigSort) >>= (unlines >>> writeFile dst)
DM>2. Изменить первую программу так, чтобы будучи запущена с ключом -u она создавала файл А2, содержащий все уникальные строки из А в алфавитном порядке и количество повторений для каждой строки.
main2 = do
args <- getArgs
let src = head args
let dst = src ++ "_sorted"let dup = src ++ "_dup"let needDup = any (=="-u") args
sortedData <- readFile src >>= (lines >>> bigSort)
hdst <- openFile dst WriteMode
hdup <- openFile dup WriteMode
-- количество одинаковых строк и сами эти строкиlet gs = [(length g, head g) | g <- group sortedData]
-- пишем параллельно в sorted и duplet writeGroup (count, str) = do
replicateM_ count (hPutStrLn hdst str)
if needDup then hPutStrLn hdup (show count ++ ": " ++ str) else return ()
mapM_ writeGroup [(length g, head g) | g <- group sortedData]
hClose hdup
hClose hdst
Здравствуйте, BulatZiganshin, Вы писали:
BZ>если ограничивать число строк, а не объём занимаемой памяти — то нормально
main = do-- (Лениво) создаём отсортированные подсписки
sublists <- fmap sort1 getContents
-- Записываем их в файлы с именами 1..n
zipWithM_ writeFile (map show [1..]) sublists
-- Читаем (опять же лениво) содержимое всех ранее созданных файлов
sublists <- mapM (fmap lines readFile.show) [1..length sublists]
-- Сливаем их и записываем результат
putStr (unlines (mergesort' compare sublists)
BZ>на самом деле, даже если я ни в чём не ошибся — этот код будет держать все данные в памяти до победного. extra points — за изящное решение этой маааленькой проблемы
Я, вроде, разобрался в чем тут проблемы — все дело в length sublists. Из-за этого пропадает ленивость.
Если же переписать вот так, то все будет работать:
main = do-- (Лениво) создаём отсортированные подсписки
sublists <- fmap sort1 getContents
-- Записываем их в файлы с именами 1..ncount <- fmap length $ zipWithM writeFile (map show [1..]) sublists
-- Читаем (опять же лениво) содержимое всех ранее созданных файлов
sublists <- mapM (fmap lines readFile.show) [1..count]
-- Сливаем их и записываем результат
putStr (unlines (mergesort' compare sublists)
Всё таки для Haskell ограничения по памяти противопоказаны
Здравствуйте, Beam, Вы писали:
BZ>>на самом деле, даже если я ни в чём не ошибся — этот код будет держать все данные в памяти до победного. extra points — за изящное решение этой маааленькой проблемы
B>Я, вроде, разобрался в чем тут проблемы — все дело в length sublists. Из-за этого пропадает ленивость. B>Если же переписать вот так, то все будет работать:
Здравствуйте, BulatZiganshin, Вы писали:
BZ>ещё одна задача из моей практики. есть список файлов, каждый файл представлен структурой, содержащей поля ext (расширение файла), size (его размер) и другие, нам неинтересные. нужно разбить этот список на группы (солид-блоки) по критериям, описанным строкой вида [Nf][Mb][e], где N и M — некоторые числа. эта запись означает, что каждая группа может содержать не более N файлов, суммарным объёмом не более M байт, с одинаковым расширением. все три части этой строки необязательны — при их отсутствии соответствующий критерий не применяется
Окамл:
(*базовый вещи *)open ExtString;;
let (|>) x f = f x;;
let (>>) f g x = g (f x);;
(* структура с информацией о файле *)type file_t = { name : string; size : int; ext : string };;
(* структура-аккумулятор с текущим блоком файлов *)type accum_t = { files : file_t list; num : int; totalsize : int};;
let new_acc = { files = []; num = 0; totalsize = 0 };; (* пустой блок *)let accumulate acc file =
{ files = file::acc.files; num = acc.num + 1; totalsize = acc.totalsize + file.size };;
(* разбор строки параметров *)let parse_param_string str =
let numparam ch =
let rec loop i lst =
if i<0 then lst else
if str.[i]>='0' && str.[i]<='9'then loop (i-1) (str.[i] :: lst) else lst in
try Some (loop (String.index str ch - 1) [] |> String.implode |> int_of_string)
with Not_found -> None in
numparam 'f', numparam 'b', String.contains str 'e';;
(* основная функция. из списка файлов делает список списков файлов согласно параметрам из строки *)let make_blocks param_str file_list =
if param_str = ""then [file_list] else(* простая оптимизация, можно и без нее *)let num_opt, size_opt, ext_opt = parse_param_string param_str in(* разбираем параметры *)
(* создаем предикаты, говорящие можно ли поместить файл в текущий блок *)let always = (fun acc file -> true) in
let size_pred = Option.map_default (fun size_limit -> (fun a f-> a.totalsize + f.size <= size_limit)) always size_opt
and ext_pred = if ext_opt then (fun a f -> if a.num > 0 then (List.hd a.files).ext = f.ext else true) else always
and num_pred = Option.map_default (fun num_limit -> (fun a f-> a.num + 1 <= num_limit)) always num_opt in(* можно, если все три предиката не возражают *)let can_accum = (fun acc file -> List.for_all (fun pred-> pred acc file) [ext_pred; size_pred; num_pred]) in(* обработка одного файла: *)let process (acc, res_list) file =
if can_accum acc file then accumulate acc file, res_list
else accumulate new_acc file, (acc::res_list) in(* обработка всего списка: собираем новый список fold'ом, разворачиваем и убираем лишнее rev_map'ом *)let acc, reslst = List.fold_left process (new_acc, []) file_list in
acc::reslst |> List.rev_map (fun acc-> List.rev acc.files);;
Здравствуйте, D. Mon, Вы писали:
DM>Использованы только иммутабельные структуры и чистые функции.
отсутствие побочных эффектов, конечно, хорошо, так как облегчает отладку, но на мой взгляд это ещё не максимум того, что можно выжать из фп подхода. для меня фп стиль — это поток преобразований входных данных в выходные, в идеале без всяких вспомогательных переменных
в данном случае это может быть поток преобразования от строки критериев к *функции* разбиения на подсписки. и состоять он может из таких шагов:
1) разбить строку на отдельные критерии
2) преобразовать каждый из них в функцию разбиения на [ленивые] подсписки по одному этому критерию
3) скомбинировать эти функции в итоговую
и вообще задача тренинга, как я её себе ставил — помочь перейти от мышления, ориентированного на манипуляцию данными, к мышлению, ориентированному на манипуляцию функциями
Здравствуйте, BulatZiganshin, Вы писали:
BZ>2) преобразовать каждый из них в функцию разбиения на [ленивые] подсписки по одному этому критерию BZ>3) скомбинировать эти функции в итоговую
Хм, если они будут разбивать независимо друг от друга, получится плохо:
Пусть 1-я функция разбивает по количеству файлов, а вторая — по суммарному размеру. Тогда их разбиения могут выглядеть так:
1) 11112222333344445555
2) 11222233334444555567
Что будет результатом комбинирования?
Здравствуйте, D. Mon, Вы писали:
DM>Пусть 1-я функция разбивает по количеству файлов, а вторая — по суммарному размеру. Тогда их разбиения могут выглядеть так: DM>1) 11112222333344445555 DM>2) 11222233334444555567 DM>Что будет результатом комбинирования?
подумай. я тебе по секрету скажу, что у меня так и работает. ключевое слово — ленивые списки
Ленивые списки — подход к реализации. Если результатом комбинирования
11112222333344445555
11222233334444555567
будет
11223344556677... (пересечение)
То это явно неверный ответ. А если не пересечение, то пойду думать..
Здравствуйте, D. Mon, Вы писали:
DM>Ленивые списки — подход к реализации.
ленивые списки — это другая парадигма программирования. к примеру, минимум в списке можно узнать как head.sort . при этом время вычисления будет O(N) — нет нужды вычислять остаток отсортированного списка, так как он всё равно не используется
Б> к примеру, минимум в списке можно узнать как head.sort . при этом время вычисления будет O(N)
Только, если функция сортировки достаточно ленивая. Одних ленивых списков недостаточно.
Сделал решение на ленивых списках. Из каждого компонента строки-параметра получается функция, которая берет ленивый список файлов и возвращает в виде ленивого списка файлов его начало — столько, сколько удовлетворяют критерию помещения в один блок (будь то количество, размер или что-то еще). Комбинация этих функций строится путем простой композиции. Результат композиции — функция, возвращающая столько файлов из начала данного ей списка, сколько удовлетворяют всем критериям. Все происходит лениво, лишние файлы не обрабатываются. Используя такую функцию формирования блока, главная функция откусывает от исходного списка файлов по блоку до тех пор, пока тот не исчерпается.
(* определение файла и функция парсинга параметров те же, их не привожу*)
(* универсальная функция выбора одного блока из начала списка файлов по критериям, заданным функциями-параметрами *)
(* принимает ленивый список файлов, возвращает тоже ленивый список файлов *)
(* ленивый список определен как type 'a llist = Nil | Cons of 'a * 'a llist Lazy.t;; *)let split_by accfun pred init file_llist =
let rec split acc = function
| Nil -> Nil
| Cons(h,t)-> let acc' = accfun acc h in
if pred acc' then Cons(h, lazy(split acc' (Lazy.force t))) else Nil in
split init file_llist;;
(* функции формирования блока, полученные путем уточнения предыдущей *)let split_by_size size_limit = split_by (fun sz h-> sz + h.size) (fun sz-> sz <= size_limit) 0;;
let split_by_num num_limit = split_by (fun n h-> n+1) (fun n-> n <= num_limit) 0;;
let split_by_ext = function
| Nil -> Nil
| Cons(h,t) -> Cons(h, lazy(split_by (fun _ f -> f.ext) ((=) h.ext) "" (Lazy.force t)));;
(* главная функция, интерфейс не менялся: превращает список в файлов в список списков *)let make_blocks2 param_str =
let num_opt, size_opt, ext_opt = parse_param_string param_str in(* разбираем параметры *)let split = (* строим одну комбинированную функцию получения блока как композицию *)
[Option.map split_by_num num_opt; (* функций, определенных параметрами *)
Option.map split_by_size size_opt;
if ext_opt then Some split_by_ext else None]
|> List.fold_left (fun spt so-> Option.map_default ((>>) spt) spt so) Std.identity in
let rec loop blocks flist = (* формируем список блоков *)let block = split flist in let len = length block in
if len=0 then blocks else loop (to_list block :: blocks) (drop len flist) in
of_list >> loop [] >> List.rev;;
Результат работы тот же, что раньше.
Использовал самописные ленивые списки:
type 'a llist = Nil | Cons of 'a * 'a llist Lazy.t;;
let rec of_list = function
| [] -> Nil
| h::t -> Cons(h, lazy (of_list t));;
let rec to_list = function
| Nil -> []
| Cons(h,t) -> h :: to_list (Lazy.force t);;
let rec take n lst =
if n = 0 then Nil else
match lst with
| Nil -> Nil
| Cons(h,t) -> Cons(h, lazy(take (n-1) (Lazy.force t)));;
let rec drop n lst =
if n = 0 then lst else
match lst with
| Nil -> Nil
| Cons(h,t) -> drop (n-1) (Lazy.force t);;
let rec iter f = function
| Nil -> ()
| Cons(h, t) -> f h; iter f (Lazy.force t);;
let rec filter p = function
| Nil -> Nil
| Cons(h,t)-> if p h then Cons(h, lazy (filter p (Lazy.force t))) else filter p (Lazy.force t);;
let rec map f = function
| Nil -> Nil
| Cons(h,t) -> Cons(f h, lazy(map f (Lazy.force t)));;
let rec append a b =
match a with
| Nil -> b
| Cons(h,t) -> Cons(h, lazy (append (Lazy.force t) b));;
let (++) = append;;
let rec length = function
| Nil -> 0
| Cons(h,t) -> 1 + length (Lazy.force t);;
let rec sort = function
| Nil -> Nil
| Cons(s,t) -> let xs = Lazy.force t in
(sort (filter (fun x-> x<s) xs)) ++ Cons(s, lazy (sort (filter (fun x-> x>=s) xs)));;
let rec nats_from n = Cons(n, lazy (nats_from (n+1)));;
let rec random k = Cons(k mod 100, lazy (rnd ((k*4021+6619) mod 10000)));;
let print_list lst = iter (Printf.printf "%d ") lst; print_endline "";;
Здравствуйте, D. Mon, Вы писали:
DM>Сделал решение на ленивых списках. Из каждого компонента строки-параметра получается функция, которая берет ленивый список файлов и возвращает в виде ленивого списка файлов его начало — столько, сколько удовлетворяют критерию помещения в один блок (будь то количество, размер или что-то еще). Комбинация этих функций строится путем простой композиции. Результат композиции — функция, возвращающая столько файлов из начала данного ей списка, сколько удовлетворяют всем критериям.
ах, красавчег. а я до этого не додумался, использовал (take.min.map length). с использованием твоего трюка полное решение на хаскеле выглядит так:
data File = File {size :: Integer, ext :: String}
-- |Разбивает список файлов по комплексному критерию типа "e10f1000b"
chopper crits = recursively (\xs -> xs.$ splitAt (length (headByCrits crits xs) `max` 1))
-- |Преобразует список критериев в функцию, возвращающую начало списка, которое им всем удовлетворяет
headByCrits = reverse -- -> b0001f01e
>>> recursively (break1 isAlpha) -- -> b0001,f01,e
>>> map headByCrit -- -> takeBytes 1000, take 10, takeOneExt
>>> flip (foldr ($)) -- -> takeBytes 1000 . take 10 . takeOneExt
-- |Преобразует (реверсированный) критерий в функцию, возвращающую соответствующее начало списка
headByCrit "e" = head . groupOn ext
headByCrit ('f':n) = take (read $ reverse n)
headByCrit ('b':n) = takeBytes (read $ reverse n)
-- |Возвращает префикс списка файлов с суммарной длиной не более bytes байт
takeBytes bytes files = files.$ take (files.$ prefixLen size (+) (<=bytes))
-- |Возвращает длину префикса списка, удовлетворяющего заданной комбинации условий
prefixLen mapper combinator tester = length . takeWhile tester . scanl1 combinator . map mapper
-- |Аналог break, который безусловно включает первый символ в первый список
break1 p (x:xs) = mapFst (x:) (break p xs)
BZ> сравнить фп и императивные решения на "рабочих" языках
вот "рабочий" язык шелл:
> есть список слов. надо записать в файл "a" все слова, начинающиеся на букву 'a', в файл "b" — слова на 'b' и т.д. вплоть до z
cat words | tr -dc a-z\\n | sed 's/^\(.\)\(.*\)$/echo \1\2 >> \1/' | sh -s
>> есть список слов. надо записать в файл "a" все слова, начинающиеся на букву 'a', в файл "b" — слова на 'b' и т.д. вплоть до z D>cat words | tr -dc a-z\\n | sed 's/^\(.\)\(.*\)$/echo \1\2 >> \1/' | sh -s
Здравствуйте, D. Mon, Вы писали:
DM>Предлагаю продолжить на примере такой задачи:
DM>1. Есть большой текстовый файл A (несколько гигов, заведомо не помещается в память), нужно сделать текстовый файл A1, содержащий все строки из А, но отсортированные в алфавитном порядке. Ограничение по памяти — 256 МБ.
— генераторы очень помогли в процессе.
Свойства получившейся программы: использует строго не более N Мб ОЗУ (по умолч. 128). Сливаются одновременно не более D (= 4) отсортированных файлов. Максимальный размер строки не более ((N << 19) / D). Ключем считается вся строка вместе с терминальным '\n', дубликаты строк отбрасываются.
Тесты:
Файл (исходники .cs) 50М, после сортировки 16М, N = 1: 6.53 sec; сортировка отсортированного 16М файла, N = 1: 4.21.
Те же файлы, N = 16: 3.29 и 1.64 сек.
Файл (.mp3) 2900M, после сортировки 2680М, N = 256: 17 mins; сортировка отсортированного 2680М файла, N = 256: 12 mins.
Файл (строки Фибоначчи случайной длины, программа генерации) 1024М, после сортировки 884M, N = 128: 200 sec.