Яка суєта з Haskell? [зачинено]


109

Я знаю кількох програмістів, які продовжують говорити про Haskell, коли вони перебувають між собою, і тут на ТАК всі, здається, люблять цю мову. Бути доброю в Haskell здається дещо схожою ознакою геніального програміста.

Чи може хтось навести кілька прикладів Haskell, які показують, чому він такий елегантний / вищий?

Відповіді:


134

Те, як це було розказано мені, і те, що я вважаю правдивим після того, як протягом місяця працювали над навчанням на Haskell, - це те, що функціональне програмування крутить ваш мозок цікавими способами: це змушує вас думати про знайомі проблеми різними способами : замість циклів, подумайте про карти та складки та фільтри тощо. Загалом, якщо у вас є декілька поглядів на проблему, це дає змогу краще обґрунтувати цю проблему та переключити точки зору по мірі необхідності.

Інша дійсно акуратна річ щодо Haskell - це її система типів. Це суворо набрано, але двигун виводу типу відчуває себе програмою Python, яка магічно повідомляє вам про помилку, пов’язану з типом. Повідомлень про помилки Haskell в цьому плані дещо не вистачає, але, коли ви більше ознайомитеся з мовою, ви скажете собі: саме так слід вводити текст!


47
Слід зазначити, що повідомлень про помилки Haskell не бракує, ghc є. Стандарт Haskell не вказує, як виконуються повідомлення про помилки.
PyRulez

Для таких плебсів, як я, GHC означає «Компілятор Глазго Хаскелла». en.wikipedia.org/wiki/Glasgow_Haskell_Compiler
Lorem Ipsum

137

Це той приклад, який переконав мене навчитися Haskell (і хлопчик, я радий, що я це зробив).

-- program to copy a file --
import System.Environment

main = do
         --read command-line arguments
         [file1, file2] <- getArgs

         --copy file contents
         str <- readFile file1
         writeFile file2 str

Гаразд, це коротка програма для читання. У цьому сенсі це краще, ніж програма C. Але чим це так відрізняється від (скажімо) програми Python з дуже подібною структурою?

Відповідь - лінива оцінка. У більшості мов (навіть у деяких функціональних) програма, структурована на зразок вище, призведе до того, що весь файл завантажується в пам'ять, а потім знову виписується під новою назвою.

Хаскелл "ледачий". Він не обчислює речі, поки цього не потрібно, а розширення не обчислює речі, які йому ніколи не потрібні. Наприклад, якби ви видалили writeFileрядок, Haskell не перешкоджав би читання нічого з файлу в першу чергу.

В даний час Haskell розуміє, що це writeFileзалежить від readFile, і тому здатний оптимізувати цей шлях даних.

Хоча результати залежать від компілятора, що, як правило, відбудеться при запуску вищевказаної програми, це таке: програма зчитує блок (скажімо, 8 КБ) першого файлу, потім записує його у другий файл, потім читає ще один блок з першого файл, і записує його у другий файл тощо. (Спробуйте запустити straceйого!)

... що дуже схоже на те, що дозволить зробити ефективна C-версія копії файлів.

Отже, Haskell дозволяє писати компактні, читаються програми - часто, не приносячи великої продуктивності.

Ще одне, що я мушу додати, це те, що Haskell просто ускладнює написання програм-баггі. Дивовижна система типу, відсутність побічних ефектів і, звичайно, компактність коду Haskell зменшує помилки як мінімум з трьох причин:

  1. Кращий дизайн програми. Знижена складність призводить до меншої кількості логічних помилок.

  2. Компактний код. Менш рядків для помилок існує.

  3. Помилки компіляції. Багато помилок просто не дійсні Haskell .

Haskell не для всіх. Але всі повинні спробувати.


Як саме ви змінили б константу 8KB (або що б там не було)? Тому що я ставлю на облік, що реалізація Haskell буде повільнішою, ніж версія C, інакше, особливо без попереднього завантаження ...
user541686

1
@Mehrdad Ви можете змінити розмір буфера за допомогою hSetBuffering handle (BlockBuffering (Just bufferSize)).
Девід

3
Дивовижно, що ця відповідь має 116 відгуків, але все, що там, є просто неправильним. Ця програма буде читати весь файл, якщо ви не використовуєте ледачих Bytestrings (з якими ви можете це зробити Data.Bytestring.Lazy.readFile), які не мають нічого спільного з тим, що Haskell є лінивою (не суворою) мовою. Монади є послідовними - це означає приблизно "всі побічні ефекти робляться, коли ви виймаєте результат". Що стосується магії "ледачого байстрингу": це небезпечно, і ви можете зробити це за допомогою подібного чи більш простого синтаксису на більшості інших мов.
Jo So

14
Нудний старий стандарт readFileтакож робить лінивий IO так само, як Data.ByteString.Lazy.readFileце робить. Тож відповідь не є помилковою, і це не просто оптимізація компілятора. Дійсно, це частина специфікації Haskell : " readFileФункція зчитує файл і повертає вміст файлу у вигляді рядка. Файл читається ліниво, на вимогу, як і getContents".
Даніель Вагнер

1
Я думаю, що інші відповіді вказують на речі, які є більш особливими щодо Haskell. Багато мов / середовищ мають потоки, ви можете зробити щось подібне в Node : const fs = require('fs'); const [file1, file2] = process.argv.slice(2); fs.createReadStream(file1).pipe(fs.createWriteStream(file2)). У Баша є щось подібне:cat $1 > $2
Макс Хейбер

64

Ви ніби не задаєте неправильне запитання.

Haskell - це не мова, на якій ти переглядаєш кілька прикольних прикладів і йдеш "ага, я бачу, ось це робить це добре!"

Це більше схоже на те, що у нас є всі ці інші мови програмування, і вони всі більш-менш схожі, і тоді є Haskell, який абсолютно інший і хитрий, що абсолютно дивовижно, коли ти звикнеш до неслухняності. Але проблема полягає в тому, що до примхливості потрібно приділити досить багато часу. Те, що відрізняє Haskell від майже будь-якої іншої мови, навіть напівпотокової:

  • Ледача оцінка
  • Ніяких побічних ефектів (усе чисто, IO / тощо відбувається через монади)
  • Неймовірно виразна система статичного типу

а також деякі інші аспекти, які відрізняються від багатьох основних мов (але деякими поділяються):

  • функціональний
  • значний пробіл
  • тип висновку

Як відповіли деякі інші афіші, поєднання всіх цих функцій означає, що ви думаєте про програмування зовсім по-іншому. І тому важко придумати приклад (або набір прикладів), який адекватно повідомляє це Джо-мейнстріму-програмісту. Це досвід. (Щоб зробити аналогію, я можу показати вам фотографії моєї поїздки 1970 року до Китаю, але, побачивши фотографії, ви все одно не дізнаєтесь, що це було, як жив там за той час. Так само я можу показати вам Haskell 'quicksort', але ви все одно не будете знати, що означає бути Haskeller.)


17
Я не згоден з вашим першим реченням. Я був дуже вражений кількома прикладами коду Haskell спочатку, і те, що насправді переконало мене, що варто вивчити, ця стаття: cs.dartmouth.edu/~doug/powser.html Але, звичайно, це цікаво для математика / фізика. Програміст, що розглядає речі реального світу, вважає цей приклад смішним.
Рафаель С. Кальсаверіні

2
@Rafael: Це ставить питання "чим би вразив програміст, що вивчає речі реального світу"?
JD

Гарне питання! Я не програміст "реального світу", тому не знаю, що їм подобається. ха-ха ... Я знаю, що люблять фізики та математики. : P
Рафаель С. Кальсаверіні

27

Що насправді роз'єднує Haskell - це зусилля, які він докладає у своїй розробці для забезпечення функціонального програмування. Ви можете програмувати у функціональному стилі майже будь-якою мовою, але все це занадто просто відмовитись з першої зручності. Haskell не дозволяє відмовитися від функціонального програмування, тому ви повинні прийняти його до логічного завершення, що є остаточною програмою, про яку легше міркувати, і уникає цілого класу найстрашніших типів помилок.

Що стосується написання програми для використання в реальному світі, то, можливо, ви знайдете, що Хаскелл не вистачає практичних дій, але ваше остаточне рішення буде кращим для того, щоб знати, що Хаскелл почав з того. Я напевно ще не там, але поки що навчання Haskell було набагато просвітнішим, ніж скажімо, Лісп був у коледжі.


1
Ну, завжди є можливість завжди і тільки використовувати монаду ST та / або unsafePerformIOдля людей, які просто хочуть спостерігати, як світ горить;)
sara

22

Частина суєти полягає в тому, що чистота та статична типізація дозволяють паралелізм у поєднанні з агресивними оптимізаціями. Паралельні мови зараз гарячі, багатоядерні трохи руйнівні.

Haskell дає вам більше варіантів паралелізму, ніж майже будь-яку мову загального призначення, а також швидкий компілятор нативного коду. Дійсно не існує конкуренції з таким видом підтримки паралельних стилів:

Тож якщо ви дбаєте про те, щоб зробити свою багатоядерну роботу, Haskell має що сказати. Відмінне місце для початку - підручник Саймона Пейтона Джонса про паралельне та паралельне програмування в Haskell .


"поряд зі швидким компілятором нативного коду"?
JD

Я вважаю, що дони посилаються на GHCI.
Грегорі Хіглі

3
@Jon: shootout.alioth.debian.org/u32/… Haskell досить добре справляється з перестрілками, наприклад.
Пікер

4
@Jon: Код перестрілки дуже старий і з далекого минулого, де GHC був менш оптимізуючим компілятором. Тим не менш, це доводить, що код Haskell може знизитись, щоб забезпечити продуктивність, якщо це необхідно. Новіші рішення у перестрілці є більш ідіоматичними та все ж таки швидкими.
Пік

1
@GregoryHigley Існує різниця між GHCI та GHC.
Список Джеремі


18

Я провів останній рік, вивчаючи Haskell і писав у ньому досить великий і складний проект. (Проект є автоматизованою системою торгівлі опціями, і все, починаючи від алгоритмів торгівлі до аналізу та обробки низькошвидкісних, швидкісних каналів ринкових даних, робиться в Haskell.) Це значно більш стисло і простіше зрозуміти (для тих, хто має відповідний фон), ніж версія Java, а також надзвичайно надійна.

Можливо, найбільшою виграшею для мене стала можливість модуляції управління потоком через такі речі, як моноїди, монади тощо. Дуже простим прикладом може бути моноїд замовлення; у виразі, такому як

c1 `mappend` c2 `mappend` c3

де c1і так після повернення LT, EQабо GT, c1повертаючись EQвикликає вираз для продовження, оцінки c2; якщо c2повертається LTабо GTце значення цілого, і c3не оцінюється. Такі речі стають значно складнішими і складнішими в таких речах, як монадичні генератори повідомлень та аналізатори, де я, можливо, перебуваю в різних типах стану, має різні умови переривання або може захотіти вирішити для будь-якого конкретного дзвінка, чи справді переривання означає "більше не обробляти" або означає "повернути помилку в кінці, але продовжуйте обробку для збору подальших повідомлень про помилки".

Це все, для чого потрібно певний час, і, мабуть, достатньо певних зусиль, щоб навчитися, і тому важко зробити переконливий аргумент для тих, хто ще не знає цих методів. Я думаю, що підручник " Все про монади" дає досить вражаючу демонстрацію одного аспекту цього, але я не очікував би, що хтось, хто не знайомий з матеріалом, вже "знайде його" під час першого чи навіть третього уважного читання.

У всякому разі, в Haskell також багато інших хороших речей, але це головне, яке я не бачу так часто, мабуть, тому, що це досить складно.


2
Дуже цікаво! Скільки рядків коду Haskell загалом увійшло до вашої автоматизованої торгової системи? Як ви поводилися з відмовкою та які результати роботи ви отримали? Нещодавно я думав, що Haskell має потенціал бути корисним для програмування з низькою затримкою ...
JD,

12

Для цікавого прикладу ви можете подивитися: http://en.literateprograms.org/Quicksort_(Haskell)

Що цікаво - подивитися на реалізацію різними мовами.

Що робить Haskell таким цікавим, поряд з іншими функціональними мовами, це той факт, що вам потрібно думати по-іншому, як програмувати. Наприклад, ви зазвичай не будете використовувати для циклів або під час, але будете використовувати рекурсію.

Як вже було сказано вище, Haskell та інші функціональні мови відрізняються паралельною обробкою та написанням додатків для роботи над декількома ядрами.


2
рекурсія - це бомба. це і відповідність шаблону.
Новачок Ellery

1
Позбавлення від циклів і при цьому є найважчою частиною для мене, коли пишуть функціональною мовою. :)
Джеймс Блек

4
Навчання думати в рекурсії замість петель було і для мене найважче. Коли він нарешті занурився, це була одна з найбільших епіфаній програмування, яку я коли-небудь мав.
Кріс Коннетт

8
За винятком того, що працюючий програміст Haskell рідко використовує примітивну рекурсію; Ви здебільшого використовуєте бібліотечні функції, такі як map та foldr.
Пол Джонсон

18
Мені здається більш цікавим те, що оригінальний алгоритм Хоара був зведений у цю невідповідну форму на основі списку, мабуть, щоб марно неефективні реалізації могли бути написані "елегантно" в Haskell. Якщо ви спробуєте написати справжній (на місці) квіксорт у Haskell, ви виявите, що це некрасиво, як пекло. Якщо ви спробуєте написати загальнодоступний загальнодоступний кікспорт у Haskell, ви виявите, що це насправді неможливо через давні помилки у смітнику GHC. Вітаючи хитрощі як хороший приклад для віри жебраків Хаскелла, ІМХО.
JD

8

Я не можу навести вам приклад, я хлопець з OCaml, але коли я опинився в такій ситуації, як ви, цікавість просто переймається, і я маю завантажити компілятор / перекладач і дати йому піти. Ви, ймовірно, дізнаєтесь набагато більше про сильні та слабкі сторони даної функціональної мови.


1
Не забудьте прочитати вихідний код компілятора. Це також дасть вам багато цінної інформації.
JD

7

Одне, що мені здається дуже крутим при роботі з алгоритмами або математичними проблемами, - це властива Хаскеллю ледача оцінка обчислень, яка можлива лише завдяки суворій функціональній природі.

Наприклад, якщо ви хочете обчислити всі прости, ви можете скористатися

primes = sieve [2..]
    where sieve (p:xs) = p : sieve [x | x<-xs, x `mod` p /= 0]

і результат насправді є нескінченним списком. Але Haskell оцінить його зліва направо, тому поки ви не спробуєте зробити щось, що вимагає всього списку, ви все одно можете використовувати його без того, щоб програма застрягла в нескінченності, наприклад:

foo = sum $ takeWhile (<100) primes

що підсумовує всі кошти менше 100. Це приємно з кількох причин. Перш за все, мені потрібно написати лише одну основну функцію, яка генерує всі праймери, і тоді я майже готовий працювати з праймерами. У об'єктно-орієнтованій мові програмування мені знадобиться якийсь спосіб сказати функції, скільки простих ліній вона повинна обчислити перед поверненням, або емуляція поведінки нескінченного списку з об'єктом. Інша справа, що ви, як правило, пишете код, який виражає те, що ви хочете обчислити, а не в тому, в якому порядку оцінювати речі, натомість компілятор робить це для вас.

Це корисно не лише для нескінченних списків, адже воно звикає, не знаючи про це весь час, коли не потрібно оцінювати більше, ніж потрібно.


2
Це не зовсім вірно; з поведінкою повернення урожайності C # (об'єктно-орієнтована мова), ви також можете оголосити нескінченні списки, які оцінюються на вимогу.
Джефф Йейтс

2
Гарна думка. Ви правильні, і я повинен уникати тверджень про те, що можна, а що не можна робити на інших мовах так категорично. Я думаю, що мій приклад був помилковим, але я все ж думаю, що ти щось отримуєш від способу лінивої оцінки Хаскелла: він дійсно є за замовчуванням і без жодних зусиль програміста. І це, я вважаю, пов’язане з його функціональним характером та відсутністю побічних ефектів.
оксамитування воску

8
Можливо, вам буде цікаво прочитати, чому "решето" - це не сито Ератостена
Кріс Конвей,

@Chris: Дякую, це насправді була досить цікава стаття! Вищеописана функція праймес не є тією, яку я використовував для власних обчислень, оскільки вона болісно повільна. Тим не менш, стаття підкреслює, що перевірка всіх чисел на мод - це дійсно інший алгоритм.
оксамитування воску

6

Я погоджуюся з іншими, що бачити кілька невеликих прикладів - це не найкращий спосіб показати себе Haskell. Але я все одно дам. Ось блискавичне рішення проблем 18 та 67 проекту Ейлера , яке вимагає знайти шлях з максимальною сумою від основи до вершини трикутника:

bottomUp :: (Ord a, Num a) => [[a]] -> a
bottomUp = head . bu
  where bu [bottom]     = bottom
        bu (row : base) = merge row $ bu base
        merge [] [_] = []
        merge (x:xs) (y1:y2:ys) = x + max y1 y2 : merge xs (y2:ys)

Ось повна реалізація алгоритму BubbleSearch від Леша та Міценмахера. Я використовував його для упаковки великих мультимедійних файлів для архівного зберігання на DVD без відходів:

data BubbleResult i o = BubbleResult { bestResult :: o
                                     , result :: o
                                     , leftoverRandoms :: [Double]
                                     }
bubbleSearch :: (Ord result) =>
                ([a] -> result) ->       -- greedy search algorithm
                Double ->                -- probability
                [a] ->                   -- list of items to be searched
                [Double] ->              -- list of random numbers
                [BubbleResult a result]  -- monotone list of results
bubbleSearch search p startOrder rs = bubble startOrder rs
    where bubble order rs = BubbleResult answer answer rs : walk tries
            where answer = search order
                  tries  = perturbations p order rs
                  walk ((order, rs) : rest) =
                      if result > answer then bubble order rs
                      else BubbleResult answer result rs : walk rest
                    where result = search order

perturbations :: Double -> [a] -> [Double] -> [([a], [Double])]
perturbations p xs rs = xr' : perturbations p xs (snd xr')
    where xr' = perturb xs rs
          perturb :: [a] -> [Double] -> ([a], [Double])
          perturb xs rs = shift_all p [] xs rs

shift_all p new' [] rs = (reverse new', rs)
shift_all p new' old rs = shift_one new' old rs (shift_all p)
  where shift_one :: [a] -> [a] -> [Double] -> ([a]->[a]->[Double]->b) -> b
        shift_one new' xs rs k = shift new' [] xs rs
          where shift new' prev' [x] rs = k (x:new') (reverse prev') rs
                shift new' prev' (x:xs) (r:rs) 
                    | r <= p    = k (x:new') (prev' `revApp` xs) rs
                    | otherwise = shift new' (x:prev') xs rs
                revApp xs ys = foldl (flip (:)) ys xs

Я впевнений, що цей код схожий на випадкові хитрощі. Але якщо ви прочитаєте запис у блозі Міценмахера та зрозумієте алгоритм, ви здивуєтесь, що можна упакувати алгоритм у код, нічого не сказавши про те, що ви шукаєте.

Надавши вам декілька прикладів, як ви просили, я скажу, що найкращий спосіб почати цінувати Хаскелл - це прочитати папір, яка дала мені ідеї, необхідні для написання пакета DVD: Чому питання функціонального програмування Джона Х'юза. Стаття насправді передує Haskell, але в ній блискуче пояснюються деякі ідеї, які роблять людей схожими на Haskell.


5

Для мене привабливість Haskell - це обіцянка гарантованої правильності компілятора . Навіть якщо це для чистих частин коду.

Я написав багато наукових коду моделювання, і задавалися питанням, так багато разів , якщо була помилка в моїх попередніх кодів, які можуть зробити недійсним багато поточної роботи.


6
Як це гарантує правильність?
Джонатан Фішофф

Чисті частини коду набагато безпечніші, ніж нечисті. Рівень довіри / вкладених зусиль набагато вищий.
rpg

1
Що справило на вас таке враження?
JD

5

Я вважаю, що для певних завдань я надзвичайно продуктивний з Haskell.

Причина в тому, що синтаксис є легким і простота тестування.

Ось як виглядає синтаксис оголошення функції:

foo a = a + 5

Це найпростіший спосіб, яким я можу придумати визначення функції.

Якщо я напишу зворотне

оберненоFoo a = a - 5

Я можу перевірити, чи є вона оберненою для будь-якого випадкового введення, написавши

prop_IsInverse :: Double -> Bool
prop_IsInverse a = a == (зворотнийFoo $ foo a)

І дзвонить з командного рядка

jonny @ ubuntu: runhaskell quickCheck + імена fooFileName.hs

Що дозволить перевірити, чи зберігаються всі властивості мого файлу, випадковим чином тестуючи входи в сто разів.

Я не вважаю, що Haskell є ідеальною мовою для всього, але якщо говорити про написання невеликих функцій та тестування, я нічого кращого не бачив. Якщо у вашому програмуванні є математичний компонент, це дуже важливо.


Які проблеми ви вирішуєте та які ще мови ви спробували?
JD

1
3D графіка в режимі реального часу для мобільних пристроїв та iPad.
Джонатан Фішофф

3

Якщо ви можете обернути голову навколо системи типу в Haskell, я думаю, що саме по собі це ціле досягнення.


1
Що там дістати? Якщо потрібно, подумайте "data" == "class" та "typeclass" = "інтерфейс" / "роль" / "черта". Це не могло бути простішим. (Немає навіть "нуля", щоб зіпсувати вас. Null - це концепція, яку ви можете самостійно вбудувати у свій тип.)
jrockway

8
Є ще багато чого, щоб дістатись, доріжки. Хоча ми з вами вважаємо це досить простим, багато людей - навіть багато розробників - знаходять певні види абстракцій дуже важко зрозуміти. Я знаю багатьох розробників, які все ще не дуже розуміють уявлення про покажчики та посилання на більш основних мовах, хоча вони використовують їх щодня.
Грегорі Хіглі

2

він не має контурних конструкцій. не багато мов мають цю особливість.


17
ghci>: m + Control.Monad ghci> forM_ [1..3] надрукувати 1 2 3
зустріч

1

Я погоджуюся з тими, хто сказав, що функціональне програмування перекручує ваш мозок у тому, щоб бачити програмування з іншого боку. Я використовував це лише як хобі, але думаю, що це принципово змінило спосіб підходу до проблеми. Я не думаю, що я був би не настільки ж ефективний з LINQ, не піддавшись впливу Haskell (і використовуючи генератори та списки розумінь в Python).


-1

Для висловлення протилежної точки зору: Стів Йегге пише, що мовам Гінделі-Мілнера не вистачає гнучкості, необхідної для написання гарних систем :

HM дуже симпатичний, у абсолютно марному формальному математичному сенсі. Він дуже добре обробляє декілька обчислювальних конструкцій; Особливо зручна схема відправки, яка відповідає в Haskell, SML та OCaml. Не дивно, що він обробляє деякі інші поширені та дуже бажані конструкції в кращому випадку незручно, але вони пояснюють ці сценарії далеко, кажучи, що ви помиляєтесь, ви насправді не хочете їх. Ви знаєте, такі речі, як, о, встановлення змінних.

Хаскеллу варто вчитися, але у нього є свої слабкі сторони.


5
Хоча це, безумовно, правда, що системи сильного типу, як правило, вимагають дотримуватися їх (саме це робить їх міцність корисною), але так само буває, що багато (більшість?) Існуючих систем типу HM, насправді, мають якісь " escape hat ', як описано у посиланні (візьміть Obj.magic в O'Caml як приклад, хоча я ніколи не використовував його, окрім як хак); на практиці, однак, для багатьох видів програм ніколи не потрібен такий пристрій.
Зак Сніг

3
Питання про те, чи є бажане встановлення змінних, залежить від того, наскільки больовим є використання альтернативних конструкцій проти того, скільки болю викликано використанням змінних. Це не для того, щоб відкинути весь аргумент, а скоріше вказати на те, що прийняття твердження "змінні є дуже бажаною конструкцією" як аксіома не є основою когерентного аргументу. Це просто так, як більшість людей навчаються програмувати.
gtd

5
-1: Заяви Стіва частково застаріли, але здебільшого просто абсолютно фактично неправильні. Розслаблене значення OCaml та система типів .NET - це явні зустрічні приклади його тверджень.
JD

4
У Стіва Йегге є нерозумна бджола в капелюшку щодо статичного набору тексту, і це не тільки те, що він говорить про це неправильно, він також продовжує виховувати це при будь-якій доступній можливості (і навіть у деяких недоступних). Ви б добре довіряли лише власному досвіду з цього приводу.
ShreevatsaR

3
Хоча я не погоджуюся з Yegge щодо статичного проти динамічного набору тексту, Haskell має тип Data.Dynamic. Якщо ви хочете динамічного набору тексту, ви можете його мати!
jrockway
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.