Эзотерическое программирование на языке Scala
Когда-то один индейский программист, разочаровавшись в технологиях белых людей, разработал свой язык программирования — Дакотан, целиком построенный на конструкциях своего родного языка. Этот язык лёг в основу первой Индейской Операционной Системы — СИУ (Система Инкорпорирующая Универсальная). Белый человек вряд ли сможет до конца осознать глубинные принципы этой системы и языка Дакотан. Тем более, он не сможет повторить эту разработку и сделать свой, настолько же глубокий язык. Поэтому, мы не ставим цели воспроизвести весь Дакотан, мы лишь попробуем воспроизвести некоторые конструкции этого языка на языке программирования, более привычном для белых людей.
Для первого примера мы выбрали наиболее простую концепцию Дакотана — небинарную логику. Как известно, в традиционных для белых людей языках общеприняты логические переменные лишь с двумя значениями — true и false (истина и ложь). Однако логика человека гораздо богаче, чем её пытается представить убогая булева алгебра. Человек крайне редко мыслит в рамках одних лишь «строго да» и «строго нет» (если, конечно, этот человек не пытается отстоять какую-то либеральную идею, однако воспроизвести либеральную логику мы попробуем как-нибудь в следующий раз). Как минимум человек использует ещё два равноценных варианта — «возможно» и «я не знаю». Привнесение этих двух вариантов существенно обогатит компьютерный язык и сделает программирование гораздо более «человечным». Поэтому приступим к делу. И для реализации конструкций Дакотана возьмём наиболее приспособленный для эзотерического программирования (и при этом одновременно наиболее понятный) язык Scala.
Первым шагом будет создание нового типа данных — ствола, от которого как раз и раскинутся ветви разнообразных логических значений,
Это — абстрактный класс, в котором объявлены две логические операции && и ||. Мы могли бы добавить, кроме того, исключающее или, однако вряд ли такая операция может быть определена для небинарной логики. В общем, обойдёмся лишь двумя, ведь наша цель — показать способ конструирования такого рода логики, а не сконструировать все её возможные нюансы.
Многие читатели заметили странное слово «sealed» в начале объявления класса. Это слово означает «герметичный». А наша логика как раз и должна быть герметичной. Совершенно незачем позволять непосвящённым добавление новых значений к нашей логике. Конечно, индейцы сделали бы иначе, — не в их правилах загонять окружающих в резервации, — однако мы, европейцы, ради вящей безопасности, всё-таки загерметизируем всех наследников базового абстрактного класса в одном файле. Заодно компилятор получит возможность предупреждать незнакомых с продвинутой логикой программистов о том, что они при описании реакции на результат забыли рассмотреть некоторые варианты. А то ведь многие наверняка по привычке подумают, что в логике только два значения — true и false, упустив другие варианты. Тут-то компилятор и скажет им: «эй-эй, ребята, а что же вы будете делать, если вдруг функция вам ответит "я не знаю"?».
После того, как мы задекларировали интерфейс для всех значений логики Дакотана, попробуем реализовать наиболее понятные из них. Те самые, к которым мы привыкли, — true и false.
Как мы видим, реализация не заняла много времени: для реализации методов мы всего-то скопировали их объявления и через знак «равно» приписали возвращаемый результат. «Но куда же делся возвращаемый тип?» — спросит внимательный читатель? «Он стал неявным», — ответим мы. Это только в объявлении методов следовало указывать тип — ведь там компилятор никак не мог узнать, какого типа значение этот метод возвращает. Когда же у методов появились тела, компилятор, который не дурак, из этих тел сам вычисляет возвращаемое значение. А раз оно совпадает с заявленным при объявлении методов в абстрактом классе, то и всё в порядке. Как говорят индейцы: «сущность человека познаётся по его телу, а не по фамилии».
Слово «class», как мы видим, тоже куда-то исчезло. Но неявным, в отличие от возвращаемого типа, оно не стало. Оно, напротив, сменилось словом «object». Что означает: «класс, экземпляр которого с самого начала существует и при этом в единственном экземпляре». Ведь напиши мы перед «True» слово «class», мы бы могли при помощи «new True» насоздавать сколько угодно Истин, тогда как Истина — всего одна. Диалектика индейцев, впрочем, учит нас, что и Ложь тоже всего одна. Нечего, знаете ли, плодить ложь.
Теперь когда кто-то хочет сказать «это — истина», он пишет:
безо всяких там «new» и прочих созданий альтернативной Истины.
Дальше больше: если есть у нас ещё и второе значение
то мы теперь можем запросто понять, а в сумме они что дают?
Как вы догадались, в сумме, конечно, будет True — та самая единственная Истина. Однако всё равно гложут нас сомнения: мы ведь с самого начала знали, что в результате получится Истина — она ещё в a была сокрыта. Сразу можно было бы дать ответ: тут Истина. Вместо этого мы зачем-то полезли проверять ещё и b. Когда b — константа, можно даже и проверить, но а вдруг выражение b — вычислямое? И при его вычислении, если Истина была в a, то при вычислении b вообще ошибка возникнет?
Как говорит народная индейская мудрость: не надо измерять длину хвоста неубитого медведя. То есть, второе выражение следует вычислять только в том случае, если с первым не всё ясно. А раз так, то и в методы мы должны передавать не результат, а само выражение.
Стрелочка после двоеточия — это как раз передача выражения и есть. Подразумевается под ним «какая-то штука, которая своим результатом имеет нечто с типом DakotanLogic». Под такое и переменные подойдут, и вызовы функций, и прямо текстовые выражения тоже, такие как «s.length == 0». И вычислится всё это только тогда, когда ему надо будет вычислиться.
Добавим теперь третье значение в нашу логику: «я не знаю». Но с ним у нас возникнет проблема — первых-то двух значений было всего два. И мы сразу могли получить результат, не взирая на обилие вариантов. Когда и так понятно, то сразу значение вернуть, когда ещё второе проверить надо — проверить его и вернуть результат. С двумя значениями всё просто — именно поэтому европейцы и рассматривают всего два возможных значения. С тремя же надо уже шансы прикидывать: если одно значения пришло, то вот так, если другое — вот эдак, третье — ещё как-то.
Шансы мы прикидываем при помощи заклинания «match». Это заклинание угадывает, что к чему, ещё лучше, чем switch в Яве и С++. Нет, match ещё ловчее, он может и тип проверить, и с объектом сравнить, и даже с переменной. В данном же случае мы написали «match» просто чтобы не писать длинной цепочки if-else. В таком виде, оно всё гораздо понятнее. Прямо как таблица логической функции: «DontKnow || False даёт DontKnow» и так далее.
С третьим значением, нам придётся и первые два пересмотреть — там ведь про третье ни сном, ни духом.
Но что мы видим? Мы видим, выражение теперь снова стало вычисляться в любом случае — иначе его нельзя будет проверить на соответствие объекту. Как говорят индейцы: чтобы узнать выше ли та гора, чем эта, надо сначала посмотреть на ту гору. Однако вожди и шаманы знают: не надо смотреть на ту гору, если эта — самая высокая из всех. Иными словами, нам надо отделить и так понятные случаи от непонятных. А понятных у нас тут две штуки: в одном случае всегда False, а в другом всегда True — что-то там проверять и сравнивать незачем, всё понятно и так.
Добавим теперь четвёртый вариант — «возможно». Ровно тем же способом, которым добавили третий.
Не будем пока вписывать соответствующую реакцию первых трёх вариантов на четвёртый, а вместо этого задумаемся, а надо ли нам вообще её туда вписывать? Ведь совершенно непонятно, зачем мы для варианта А пишем реакцию на вариант Б, а для варианта Б — на вариант А. Не достаточно ли нам будет только одного описания? Действительно, за исключением двух понятных случаев, все остальные, — нам вполне понятно, — в обе стороны работают одинаково. То есть, каждую из реакций мы можем описать только один раз, а обратный вариант реализовать вызовом прямого.
Так и поступим. После этого реализация простейших операций логики Дакотана примет окончательный вид.
(продолжение следует)
Для первого примера мы выбрали наиболее простую концепцию Дакотана — небинарную логику. Как известно, в традиционных для белых людей языках общеприняты логические переменные лишь с двумя значениями — true и false (истина и ложь). Однако логика человека гораздо богаче, чем её пытается представить убогая булева алгебра. Человек крайне редко мыслит в рамках одних лишь «строго да» и «строго нет» (если, конечно, этот человек не пытается отстоять какую-то либеральную идею, однако воспроизвести либеральную логику мы попробуем как-нибудь в следующий раз). Как минимум человек использует ещё два равноценных варианта — «возможно» и «я не знаю». Привнесение этих двух вариантов существенно обогатит компьютерный язык и сделает программирование гораздо более «человечным». Поэтому приступим к делу. И для реализации конструкций Дакотана возьмём наиболее приспособленный для эзотерического программирования (и при этом одновременно наиболее понятный) язык Scala.
Первым шагом будет создание нового типа данных — ствола, от которого как раз и раскинутся ветви разнообразных логических значений,
sealed abstract class DakotanLogic { def ||(that: DakotanLogic) : DakotanLogic def &&(that: DakotanLogic) : DakotanLogic }
Это — абстрактный класс, в котором объявлены две логические операции && и ||. Мы могли бы добавить, кроме того, исключающее или, однако вряд ли такая операция может быть определена для небинарной логики. В общем, обойдёмся лишь двумя, ведь наша цель — показать способ конструирования такого рода логики, а не сконструировать все её возможные нюансы.
Многие читатели заметили странное слово «sealed» в начале объявления класса. Это слово означает «герметичный». А наша логика как раз и должна быть герметичной. Совершенно незачем позволять непосвящённым добавление новых значений к нашей логике. Конечно, индейцы сделали бы иначе, — не в их правилах загонять окружающих в резервации, — однако мы, европейцы, ради вящей безопасности, всё-таки загерметизируем всех наследников базового абстрактного класса в одном файле. Заодно компилятор получит возможность предупреждать незнакомых с продвинутой логикой программистов о том, что они при описании реакции на результат забыли рассмотреть некоторые варианты. А то ведь многие наверняка по привычке подумают, что в логике только два значения — true и false, упустив другие варианты. Тут-то компилятор и скажет им: «эй-эй, ребята, а что же вы будете делать, если вдруг функция вам ответит "я не знаю"?».
После того, как мы задекларировали интерфейс для всех значений логики Дакотана, попробуем реализовать наиболее понятные из них. Те самые, к которым мы привыкли, — true и false.
object False extends DakotanLogic { def ||(that: DakotanLogic) = that def &&(that: DakotanLogic) = False } object True extends DakotanLogic { def ||(that: DakotanLogic) = True def &&(that: DakotanLogic) = that }
Как мы видим, реализация не заняла много времени: для реализации методов мы всего-то скопировали их объявления и через знак «равно» приписали возвращаемый результат. «Но куда же делся возвращаемый тип?» — спросит внимательный читатель? «Он стал неявным», — ответим мы. Это только в объявлении методов следовало указывать тип — ведь там компилятор никак не мог узнать, какого типа значение этот метод возвращает. Когда же у методов появились тела, компилятор, который не дурак, из этих тел сам вычисляет возвращаемое значение. А раз оно совпадает с заявленным при объявлении методов в абстрактом классе, то и всё в порядке. Как говорят индейцы: «сущность человека познаётся по его телу, а не по фамилии».
Слово «class», как мы видим, тоже куда-то исчезло. Но неявным, в отличие от возвращаемого типа, оно не стало. Оно, напротив, сменилось словом «object». Что означает: «класс, экземпляр которого с самого начала существует и при этом в единственном экземпляре». Ведь напиши мы перед «True» слово «class», мы бы могли при помощи «new True» насоздавать сколько угодно Истин, тогда как Истина — всего одна. Диалектика индейцев, впрочем, учит нас, что и Ложь тоже всего одна. Нечего, знаете ли, плодить ложь.
Теперь когда кто-то хочет сказать «это — истина», он пишет:
val a = True
безо всяких там «new» и прочих созданий альтернативной Истины.
Дальше больше: если есть у нас ещё и второе значение
val b = False
то мы теперь можем запросто понять, а в сумме они что дают?
val res = a || b
Как вы догадались, в сумме, конечно, будет True — та самая единственная Истина. Однако всё равно гложут нас сомнения: мы ведь с самого начала знали, что в результате получится Истина — она ещё в a была сокрыта. Сразу можно было бы дать ответ: тут Истина. Вместо этого мы зачем-то полезли проверять ещё и b. Когда b — константа, можно даже и проверить, но а вдруг выражение b — вычислямое? И при его вычислении, если Истина была в a, то при вычислении b вообще ошибка возникнет?
val isEmptyString = (s == null) || (s.length == 0)
Как говорит народная индейская мудрость: не надо измерять длину хвоста неубитого медведя. То есть, второе выражение следует вычислять только в том случае, если с первым не всё ясно. А раз так, то и в методы мы должны передавать не результат, а само выражение.
sealed abstract class DakotanLogic { def ||(that : => DakotanLogic) : DakotanLogic def &&(that : => DakotanLogic) : DakotanLogic } object False extends DakotanLogic { def ||(that: => DakotanLogic) = that def &&(that: => DakotanLogic) = False } object True extends DakotanLogic { def ||(that: => DakotanLogic) = True def &&(that: => DakotanLogic) = that }
Стрелочка после двоеточия — это как раз передача выражения и есть. Подразумевается под ним «какая-то штука, которая своим результатом имеет нечто с типом DakotanLogic». Под такое и переменные подойдут, и вызовы функций, и прямо текстовые выражения тоже, такие как «s.length == 0». И вычислится всё это только тогда, когда ему надо будет вычислиться.
Добавим теперь третье значение в нашу логику: «я не знаю». Но с ним у нас возникнет проблема — первых-то двух значений было всего два. И мы сразу могли получить результат, не взирая на обилие вариантов. Когда и так понятно, то сразу значение вернуть, когда ещё второе проверить надо — проверить его и вернуть результат. С двумя значениями всё просто — именно поэтому европейцы и рассматривают всего два возможных значения. С тремя же надо уже шансы прикидывать: если одно значения пришло, то вот так, если другое — вот эдак, третье — ещё как-то.
object DontKnow extends DakotanLogic { def ||(that: => DakotanLogic) = that match { case False => DontKnow case True => True case DontKnow => DontKnow } def &&(that: => DakotanLogic) = that match { case False => False case True => DontKnow case DontKnow => DontKnow } }
Шансы мы прикидываем при помощи заклинания «match». Это заклинание угадывает, что к чему, ещё лучше, чем switch в Яве и С++. Нет, match ещё ловчее, он может и тип проверить, и с объектом сравнить, и даже с переменной. В данном же случае мы написали «match» просто чтобы не писать длинной цепочки if-else. В таком виде, оно всё гораздо понятнее. Прямо как таблица логической функции: «DontKnow || False даёт DontKnow» и так далее.
С третьим значением, нам придётся и первые два пересмотреть — там ведь про третье ни сном, ни духом.
object False extends DakotanLogic { def ||(that: => DakotanLogic) = that match { case False => False case True => True case DontKnow => DontKnow } def &&(that: => DakotanLogic) = that match { case False => False case True => False case DontKnow => False } } object True extends DakotanLogic { def ||(that: => DakotanLogic) = that match { case False => True case True => True case DontKnow => True } def &&(that: => DakotanLogic) = that match { case False => False case True => True case DontKnow => DontKnow } }
Но что мы видим? Мы видим, выражение теперь снова стало вычисляться в любом случае — иначе его нельзя будет проверить на соответствие объекту. Как говорят индейцы: чтобы узнать выше ли та гора, чем эта, надо сначала посмотреть на ту гору. Однако вожди и шаманы знают: не надо смотреть на ту гору, если эта — самая высокая из всех. Иными словами, нам надо отделить и так понятные случаи от непонятных. А понятных у нас тут две штуки: в одном случае всегда False, а в другом всегда True — что-то там проверять и сравнивать незачем, всё понятно и так.
object False extends DakotanLogic { def ||(that: => DakotanLogic) = that match { case False => False case True => True case DontKnow => DontKnow } def &&(that: => DakotanLogic) = False } object True extends DakotanLogic { def ||(that: => DakotanLogic) = True def &&(that: => DakotanLogic) = that match { case False => False case True => True case DontKnow => DontKnow } }
Добавим теперь четвёртый вариант — «возможно». Ровно тем же способом, которым добавили третий.
object Probably extends DakotanLogic { def ||(that: => DakotanLogic) = that match { case False => Probably case True => True case DontKnow => Probably case Probably => Probably } def &&(that: => DakotanLogic) = that match { case False => False case True => Probably case DontKnow => DontKnow case Probably => Probably } }
Не будем пока вписывать соответствующую реакцию первых трёх вариантов на четвёртый, а вместо этого задумаемся, а надо ли нам вообще её туда вписывать? Ведь совершенно непонятно, зачем мы для варианта А пишем реакцию на вариант Б, а для варианта Б — на вариант А. Не достаточно ли нам будет только одного описания? Действительно, за исключением двух понятных случаев, все остальные, — нам вполне понятно, — в обе стороны работают одинаково. То есть, каждую из реакций мы можем описать только один раз, а обратный вариант реализовать вызовом прямого.
Так и поступим. После этого реализация простейших операций логики Дакотана примет окончательный вид.
sealed abstract class DakotanLogic { def &&(that: => DakotanLogic): DakotanLogic def ||(that: => DakotanLogic): DakotanLogic } object False extends DakotanLogic { def ||(that: => DakotanLogic) = that match { case False => False case _ => that || this } def &&(that: => DakotanLogic) = False } object True extends DakotanLogic { def ||(that: => DakotanLogic) = True def &&(that: => DakotanLogic) = that match { case False => False case True => True case _ => that && this } } object DontKnow extends DakotanLogic { def ||(that: => DakotanLogic) = that match { case False => DontKnow case True => True case DontKnow => DontKnow case _ => that || this } def &&(that: => DakotanLogic) = that match { case False => False case True => DontKnow case DontKnow => DontKnow case _ => that && this } } object Probably extends DakotanLogic { def ||(that: => DakotanLogic) = that match { case False => Probably case True => True case DontKnow => Probably case Probably => Probably } def &&(that: => DakotanLogic) = that match { case False => False case True => Probably case DontKnow => DontKnow case Probably => Probably } }
(продолжение следует)