Lex Kravetski (lex_kravetski) wrote,
Lex Kravetski
lex_kravetski

Category:

Эзотерическое программирование на языке Scala

Когда-то один индейский программист, разочаровавшись в технологиях белых людей, разработал свой язык программирования — Дакотан, целиком построенный на конструкциях своего родного языка. Этот язык лёг в основу первой Индейской Операционной Системы — СИУ (Система Инкорпорирующая Универсальная). Белый человек вряд ли сможет до конца осознать глубинные принципы этой системы и языка Дакотан. Тем более, он не сможет повторить эту разработку и сделать свой, настолько же глубокий язык. Поэтому, мы не ставим цели воспроизвести весь Дакотан, мы лишь попробуем воспроизвести некоторые конструкции этого языка на языке программирования, более привычном для белых людей.

Для первого примера мы выбрали наиболее простую концепцию Дакотана — небинарную логику. Как известно, в традиционных для белых людей языках общеприняты логические переменные лишь с двумя значениями — 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
	}
}


(продолжение следует)
Tags: программирование, юмор
Subscribe
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 38 comments