Чому більшість основних мов не підтримують синтаксис "x <y <z" для тривимірних булевих порівнянь?


34

Якщо я хочу порівняти два числа (або інші впорядковані об'єкти), я б це зробив з x < y. Якщо я хочу порівняти три з них, студент з алгебри середньої школи запропонує спробувати x < y < z. Потім програміст у мені відповість "ні, це не вірно, ти повинен робити x < y && y < z".

Більшість мов, на які я натрапив, схоже, не підтримують цей синтаксис, що дивно, враховуючи, наскільки це поширено в математиці. Python - помітний виняток. JavaScript виглядає як виняток, але насправді це лише нещасний побічний продукт пріоритету оператора та неявних перетворень; в node.js, 1 < 3 < 2оцінює true, тому що це дійсно (1 < 3) < 2 === true < 2 === 1 < 2.

Отже, моє запитання таке: Чому x < y < zзазвичай не доступний в мовах програмування з очікуваною семантикою?


1
Ось граматичний файл, який вони зручно дотримуватися прямо в документації Python - я не думаю, що це так складно: docs.python.org/reference/grammar.html
Аарон Хол

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

@ErikEidt - Попит здатний писати математичні вирази так, як нас навчали в середній школі (або раніше). Кожен, хто схильний до математики, знає, що означає $ a <b <c <d $. Тільки те, що функція існує, не означає, що ви повинні її використовувати. Ті, кому це не подобається, завжди можуть скласти особисте або проектне правило, забороняючи його використання.
Девід Хаммен

2
Я думаю, що це зводиться до того, що команді C # (як приклад) краще вивчити LINQ, і, можливо, в майбутньому, можливо, записати типи та відповідність шаблонів, ніж додати трохи синтаксичного цукру, який би врятував людей 4 натискання клавіш, а не дуже додайте будь-якої виразності (ви також можете написати helpermethods, як, static bool IsInRange<T>(this T candidate, T lower, T upper) where T : IComparable<T>якщо це насправді турбує вас, щоб побачити &&)
sara

1
SQL досить "мейнстрім", і ви можете написати "x між 1 і 10"
JoelFan

Відповіді:


30

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

нормальне абстрактне дерево синтаксису для двійкових операторів

При оцінці (що ви робите з листя вгору) це призводить до булевого результату x < y, тоді ви отримуєте помилку типу, що намагається зробити boolean < z. Для того, x < y < zщоб ви працювали, як ви обговорювали, вам потрібно створити спеціальний випадок у компіляторі, щоб створити дерево синтаксису на зразок:

синтаксичне дерево спеціального випадку

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


1
"тоді ви отримуєте помилку типу, намагаючись зробити булеву <z" - не якщо компілятор дозволяє ланцюжок, оцінюючи y на місці для порівняння z. "Це додає багато місця для того, щоб помилитися, що мовні дизайнери скоріше уникають, якщо можливо". Власне, Python не має жодних проблем робити це, і логіка розбору обмежується однією функцією: hg.python.org/cpython/file/tip/Python/ast.c#l1122 - не так багато місця для речей. неправильно. "іноді діє як двійковий оператор, а іноді ефективно діє як тринарійний оператор", - у Python весь ланцюг порівняння є потрійним.
Aaron Hall

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

Так, але ... це вже трійчастий оператор доступний в багатьох мовах?
JensG

1
@JensG Позначення терміналу означає, що він має 3 аргументи. У контексті Вашого посилання це оператор тристрокового стану. Мабуть, "тринадцять" в терміні, придуманому для оператора, який, здається, бере 2, але насправді займає 3. Моє основне питання з цією відповіддю - це здебільшого FUD.
Аарон Хол

2
Я одна з прихильників цієї прийнятої відповіді. (@JesseTG :. ласка , unaccept цієї відповіді) Це питання плутає які x<y<zкошти, або , що більш важливо, x<y<=z. Ця відповідь трактується x<y<zяк оператор тринаціональних операцій. Саме так цей чітко визначений математичний вираз не слід тлумачити. x<y<zзамість цього є стенограма для (x<y)&&(y<z). Індивідуальне порівняння все ще є двійковим.
Девід Хаммен

37

Чому x < y < zзазвичай не доступний в мовах програмування?

У цій відповіді я роблю висновок, що

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

Вступ

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

Документація та реалізація Python

З документів / граматики ми бачимо, що ми можемо зв'язати будь-яку кількість виразів із операторами порівняння:

comparison    ::=  or_expr ( comp_operator or_expr )*
comp_operator ::=  "<" | ">" | "==" | ">=" | "<=" | "!="
                   | "is" ["not"] | ["not"] "in"

а в документації далі зазначено:

Порівняння можуть бути ланцюгові довільно, наприклад, x <y <= z еквівалентно x <y і y <= z, за винятком того, що y оцінюється лише один раз (але в обох випадках z взагалі не оцінюється, коли знайдено x <y бути помилковим).

Логічна еквівалентність

Так

result = (x < y <= z)

логічно еквівалентні з точки зору оцінки x, yі z, за винятком того , yобчислюється двічі:

x_lessthan_y = (x < y)
if x_lessthan_y:       # z is evaluated contingent on x < y being True
    y_lessthan_z = (y <= z)
    result = y_lessthan_z
else:
    result = x_lessthan_y

Знову ж таки, різниця полягає в тому, що y оцінюється лише один раз за допомогою (x < y <= z).

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

Огляд синтаксичного синтаксичного дерева

Ми можемо перевірити, як синтаксичні синтаксичні синтаксичні розробки Python:

>>> import ast
>>> node_obj = ast.parse('"foo" < "bar" <= "baz"')
>>> ast.dump(node_obj)
"Module(body=[Expr(value=Compare(left=Str(s='foo'), ops=[Lt(), LtE()],
 comparators=[Str(s='bar'), Str(s='baz')]))])"

Тож ми можемо побачити, що Python і будь-яка інша мова справді не важко розбирати.

>>> ast.dump(node_obj, annotate_fields=False)
"Module([Expr(Compare(Str('foo'), [Lt(), LtE()], [Str('bar'), Str('baz')]))])"
>>> ast.dump(ast.parse("'foo' < 'bar' <= 'baz' >= 'quux'"), annotate_fields=False)
"Module([Expr(Compare(Str('foo'), [Lt(), LtE(), GtE()], [Str('bar'), Str('baz'), Str('quux')]))])"

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

Висновок на Python

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

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

То чому x <y <z не є доступним в мовах програмування?

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

Подібні запитання можна задати про інші важливіші мовні особливості

Чому в Java чи C # не доступно багатократне успадкування? Тут немає хорошої відповіді на жодне питання . Можливо, розробники були занадто ледачими, як стверджує Боб Мартін, а наведені причини - лише виправдання. І багатократне успадкування - досить велика тема в галузі інформатики. Це, безумовно, важливіше, ніж ланцюжок операторів.

Прості шляхи вирішення існують

Ланцюжок операторів порівняння є елегантним, але аж ніяк не таким важливим, як багаторазове успадкування. І так само, як Java та C # мають інтерфейси як вирішення, так і кожна мова для декількох порівнянь - ви просто ланцюжок порівнянь з булевими "та" s, що працює досить легко.

Більшість мов регулюються комітетом

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

Чи можуть змінити мови, які не пропонують цю функцію?

Якщо мова дозволяє x < y < zбез очікуваної математичної семантики, це було б суттєвою зміною. Якби це не дозволило в першу чергу, було б майже тривіально додати.

Порушення змін

Щодо мов із порушеннями змін: ми оновлюємо мови з порушеннями поведінки, але користувачам це не подобається, особливо користувачам функцій, які можуть бути порушені. Якщо користувач покладається на колишню поведінку x < y < z, він, швидше за все, голосно протестує. І оскільки більшість мов керуються комітетом, я сумніваюся, що ми отримаємо багато політичної волі для підтримки такої зміни.


Чесно кажучи, я не несу проблеми з семантикою , передбачених мовами, операції порівняння ланцюга , такі як `х <у <г ' , але це тривіально для розробників подумки карту x < y < zдо (x < y) && (y < z). Вибираючи гниди, ментальна модель для ланцюгового порівняння - загальна математика. Класичне порівняння - це не математика взагалі, а булева логіка. x < yвиробляє двійкову відповідь {0}. y < zвиробляє двійкову відповідь {1}. {0} && {1}виробляє описову відповідь. Логіка складена, а не наївно прикута.
К. Алан Бейтс

Для кращого спілкування я відповів попередньо на одне речення, яке безпосередньо узагальнює весь зміст. Це тривале речення, тому я його розбив кулями.
Аарон Хол

2
Основна причина, по якій мало мов реалізують цю особливість, - це те, що перед Гвідо ніхто навіть не думав про це. Мови, які успадковують C, не можуть отримати це "правильне" (математично правильно) тепер, насамперед тому, що розробники C зрозуміли це "неправильно" (математично неправильно) понад 40 років тому. Там багато коду, який залежить від контрінтуїтивного характеру інтерпретації цих мов x<y<z. Язик має один раз шанс отримати щось подібне право, і такий шанс є на мові.
Девід Хаммен

1
@ K.AlanBates Ви робите 2 бали: 1) ланцюжок операторів неохайний і 2), що синтаксичний цукор не має значення. До першого: я продемонстрував, що ланцюжок операторів на 100% детермінований, чи не так? Можливо, деякі програмісти занадто подумки ледачі, щоб розширити свої можливості зрозуміти конструкцію? До другого моменту: мені здається, що ви прямо сперечаєтеся з читабельністю? Невже синтаксичний цукор зазвичай не вважається хорошою справою, коли він покращує читабельність? Якщо нормально мислити таким чином, чому б програміст не хотів так спілкуватися? Код повинен бути написаний для читання, ні?
Aaron Hall

2
I have watched both Ruby and Python implement breaking changes.Для тих, хто цікавиться, ось надзвичайна зміна C # 5.0, що включає змінні та закриття циклу: blogs.msdn.microsoft.com/ericlippert/2009/11/12/…
user2023861

13

Комп'ютерні мови намагаються визначити найменші можливі одиниці і дозволяють комбінувати їх. Найменшою можливою одиницею буде щось на зразок "x <y", що дає бульний результат.

Ви можете попросити термінального оператора. Прикладом може бути x <y <z. Тепер які комбінації операторів ми дозволяємо? Очевидно, що x> y> z або x> = y> = z або x> y> = z або, можливо, x == y == z повинно бути дозволено. Що з x <y> z? x! = y! = z? Що означає останній, x! = Y і y! = Z або що всі три різні?

Тепер просування аргументів: У C або C ++ аргументи будуть переведені на загальний тип. Отже, що значить x <y <z значення x подвійне, але y і z - це довгі довгі int? Усі троє сприяли подвоєнню? Або y приймається як подвійний раз і так довго, як інший раз? Що станеться, якщо в C ++ один або обидва оператори перевантажені?

І останнє, чи дозволяєте ви будь-яку кількість операндів? Як і <b> c <d> e <f> g?

Що ж, все стає дуже складним. Тепер я не заперечую, що x <y <z створює синтаксичну помилку. Оскільки корисність його невелика порівняно з шкодою, заподіяною початківцям, які не можуть зрозуміти, що насправді робить x <y <z.


4
Отже, коротше кажучи, це просто важка особливість добре проектувати.
Джон Перді,

2
Це насправді не є причиною пояснювати, чому не добре відома мова містить цю функцію. Власне кажучи, досить легко включити його до мови чітко визначеним чином. Це лише питання перегляду його як списку, з'єднаного операторами подібного типу, а не кожного оператора як двійкового. Те ж саме можна зробити і для сум x + y + z, з тією лише різницею, що це не означає жодної семантичної різниці. Тож це просто те, що жодна відома мова ніколи не дбала про це.
cmaster

1
Я думаю, що в Python це трохи оптимізація ( x < y < zяка еквівалентна, ((x < y) and (y < z))але yлише з оцінкою лише один раз ), яку я б уявив, що складені мови оптимізують їхній шлях. "Оскільки корисність його невелика порівняно з шкодою, заподіяною початківцям, які не можуть зрозуміти, що насправді робить x <y <z". Я думаю, що це неймовірно корисно. Напевно, збираюся -1 за це ...
Аарон Хол

Якщо мета - створити мову, яка усуває всі речі, які можуть бентежити найрозумніших програмістів, така мова вже існує: COBOL. Я вважаю за краще використовувати сам python, де справді можна писати a < b > c < d > e < f > g, з "очевидним" значенням (a < b) and (b > c) and (c < d) and (d > e) and (e < f) and (f > g). Просто тому, що ти можеш написати, що не означає, що ти повинен. Усунення таких жахливих ситуацій є сферою розгляду коду. З іншого боку, написання 0 < x < 8python має очевидне (без лапок цитат) значення, що x лежить між 0 і 8, виключно.
Девід Хаммен

@DavidHammen, за іронією долі, COBOL дійсно допускає <b <c
JoelFan

10

У багатьох мовах програмування x < y- це двійкове вираження, яке приймає два операнди і оцінює один булевий результат. Тому, якщо пов’язувати декілька виразів true < zі false < zне має сенсу, і якщо ці вирази успішно оцінюються, вони, ймовірно, дають неправильний результат.

Набагато простіше мислити x < yяк виклик функції, який приймає два параметри і дає єдиний булевий результат. Насправді саме стільки мов реалізують це під кришкою. Це комбінований, легко компілюється, і він просто працює.

x < y < zСценарій набагато складніший. Тепер компілятор, по суті, має до моди три функції: x < y, y < z, і результат цих двох значень разом операцію AND, все в контексті , можливо , граматики неоднозначного мови .

Чому вони зробили це по-іншому? Тому що це однозначна граматика, набагато простіше у виконанні та набагато легше виправити.


2
Якщо ви проектуєте мову, у вас є можливість зробити її правильним результатом.
JesseTG

2
Звичайно, це відповідає на питання. Якщо питання справді саме так , відповідь "тому, що саме це обрали дизайнери мови". Якщо ви можете придумати кращу відповідь, ніж це, продовжуйте це робити. Зауважте, що Гнашер по суті сказав саме те саме в першому абзаці своєї відповіді .
Роберт Харві

3
Знову ти розщеплюєш волоски. Програмісти, як правило, роблять це. "Ви хочете вивезти сміття?" "Ні." "Ви виймете сміття?" "Так."
Роберт Харві

2
Я також оспорюю останній абзац. Python підтримує порівняння ланцюгів, а його аналізатор - LL (1). Не обов'язково важко визначити і реалізувати семантику: Python просто каже, що e1 op1 e2 op2 e3 op3 ...це рівнозначно, за e1 op e2 and e2 op2 e3 and ...винятком того, що кожен вираз оцінюється лише один раз. (BTW це просте правило має заплутаний побічний ефект, що такі заяви, як a == b is Trueбільше, не мають наміченого ефекту.)

2
@RobertHarvey re:answerЦе мій розум негайно пішов, як і для мого коментаря до головного питання. Я не вважаю підтримкою того, x < y < zщоб додати якусь конкретну цінність до мовної семантики. (x < y) && (y < z)він ширше підтримується, є більш експлікаційним, виразнішим, легше перетравлюється в його складові, є більш складним, більш логічним, легше відновлюється.
К. Алан Бейтс

6

Більшість основних мов (принаймні частково) орієнтовані на об'єкти. По суті, основним принципом ОО є те, що об'єкти надсилають повідомлення іншим об'єктам (або самим собою), а отримувач цього повідомлення має повний контроль над тим, як реагувати на це повідомлення.

Тепер давайте подивимось, як ми могли б реалізувати щось подібне

a < b < c

Ми могли б оцінити це строго зліва направо (ліво-асоціативно):

a.__lt__(b).__lt__(c)

Але тепер ми закликаємо __lt__до результату a.__lt__(b), який є a Boolean. В цьому немає сенсу.

Спробуємо правильно-асоціативні:

a.__lt__(b.__lt__(c))

Ні, це теж не має сенсу. Тепер ми маємо a < (something that's a Boolean).

Гаразд, як щодо трактування цього синтаксичного цукру. Давайте зробимо ланцюжок з n <порівнянь, що надсилають повідомлення n-1-arry. Це може означати, ми посилаємо повідомлення __lt__до a, передаючи bі в cякості аргументів:

a.__lt__(b, c)

Гаразд, це працює, але тут є дивна асиметрія: aвирішується, чи менша вона b. Але bне добираються , щоб вирішити , чи є він менше c, а це рішення також зроблено a.

Що з інтерпретацією цього повідомлення як n-арного повідомлення this?

this.__lt__(a, b, c)

Нарешті! Це може спрацювати. Це означає, однак, що впорядкування об'єктів більше не є властивістю об'єкта (наприклад, чи aменша, ніж bце, ні властивість aні, ні b), а натомість властивість контексту (тобто this).

З точки зору мейнстріму, що здається дивним. Однак, наприклад, у Haskell, це нормально. OrdНаприклад, може бути декілька різних реалізацій класу типу, і, чи aменший він чи ні b, залежить від того, який екземпляр типу класу може бути в області застосування.

Але на самому справі, це не так, що дивно на всіх! І Java ( Comparator), і .NET ( IComparer) мають інтерфейси, які дозволяють вводити власне відношення замовлення, наприклад, в алгоритми сортування. Таким чином, вони повністю визнають, що замовлення - це не те, що закріплено за типом, а натомість залежить від контексту.

Наскільки я знаю, наразі не існує мов, які б виконували такий переклад. Однак є пріоритет: і Ioke, і Seph мають те, що їхній дизайнер називає "тринаціональними операторами" - операторами, які є синтаксично бінарними, але семантично потрійними. Зокрема,

a = b

це НЕ інтерпретуються як відправка повідомлення =для aпроходження в bякості аргументу, а також відправка повідомлення =на «поточний Ground» (поняття подібного , але не ідентичне this) проходить aі в bякості аргументів. Отже, a = bтрактується як

=(a, b)

і ні

a =(b)

Це можна легко узагальнити для n-арних операторів.

Зауважте, що це справді властиво мовам ОО. У ОО завжди у нас є один єдиний об'єкт, який в кінцевому рахунку відповідає за інтерпретацію відправки повідомлення, і, як ми бачили, це не відразу очевидно для чогось такого, a < b < cяким повинен бути об'єкт.

Однак це не стосується процедурних чи функціональних мов. Наприклад, в схемі , Common Lisp , і Clojure , то <функція п-арной, і може бути викликана з довільним числом аргументів.

Зокрема, <робить НЕ середній «менше», а ці функції інтерпретуються трохи інакше:

(<  a b c d) ; the sequence a, b, c, d is monotonically increasing
(>  a b c d) ; the sequence a, b, c, d is monotonically decreasing
(<= a b c d) ; the sequence a, b, c, d is monotonically non-decreasing
(>= a b c d) ; the sequence a, b, c, d is monotonically non-increasing

3

Це просто тому, що мовні дизайнери не думали про це чи не думали, що це гарна ідея. Python робить це так, як ви описали, з простою (майже) граматикою LL (1).


1
Це все ще буде розбиратись із звичайною граматикою майже в будь-якій основній мові; це просто не буде зрозуміло правильно з причини, яку дав @RobertHarvey.
Мейсон Уілер

@MasonWheeler Ні, не обов’язково. Якщо правила написані так, що порівняння є взаємозамінними з іншими операторами (скажімо, тому, що вони мають однаковий пріоритет), ви не отримаєте правильної поведінки. Те, що Python ставить усі порівняння на один рівень, це те, що дозволяє йому потім трактувати послідовність як сполучник.
Ніл Г

1
Не зовсім. Введіть 1 < 2 < 3у Java або C # і у вас немає проблеми з перевагою оператора; у вас проблема з недійсними типами. Проблема полягає в тому, що це все ще буде розбиратися саме так, як ви його написали, але вам потрібна спеціальна логіка в компіляторі, щоб перетворити її з послідовності окремих порівнянь на ланцюгове порівняння.
Мейсон Уілер

2
@MasonWheeler Що я говорю, це те, що мова повинна бути розроблена для того, щоб вона працювала. Однією з частин цього є отримання граматики правильно. (Якщо правила написані так, що порівняння є взаємозамінними з іншими операторами, скажімо, оскільки вони мають однаковий пріоритет, то ви не отримаєте правильної поведінки.) Інша частина цього - інтерпретація AST як сполучник, який C ++ не робить. Основний момент моєї відповіді полягає в тому, що це рішення дизайнера мови.
Ніл Г

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

2

Наступна програма C ++ компілюється з nary peep від clang, навіть із попередженнями, встановленими на найвищому можливому рівні ( -Weverything):

#include <iostream>
int main () { std::cout << (1 < 3 < 2) << '\n'; }

З іншого боку, набір компіляторів gnu чудово попереджає мене про це comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses].

Отже, моє запитання таке: чому x <y <z зазвичай не доступний в мовах програмування, з очікуваною семантикою?

Відповідь проста: сумісність назад. У дикій природі існує величезна кількість коду, який використовує еквівалент 1<3<2та очікує, що результат буде справжнім.

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


+1, оскільки ця програма виводить «1» як результат вираження, який явно не відповідає математиці. Хоча це надумано, приклад у реальному світі з незрозумілими назвами змінних став би кошмаром налагодження, якби ця мовна функція була додана.
Сет Баттін

@SethBattin - це не кошмар налагодження в Python. Єдина проблема в Python полягає в if x == y is True : ...моїй думці: люди, які пишуть такий код, заслуговують на те, що вони піддаються якимось надзвичайним, надзвичайним видам катувань, які (якби він був зараз живий) змусили б Торкемаду самому знепритомніти.
Девід Хаммен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.