Функции (ECMAScript)
Все, что вам нужно, это определения функций и вызовы функций. Вам не нужны никакие ветвления, условия, операторы или встроенные функции. Я продемонстрирую реализацию с использованием ECMAScript.
Во-первых, давайте определим две функции с именем trueи false. Мы можем определить их так, как хотим, они совершенно произвольны, но мы определим их совершенно особым образом, что имеет некоторые преимущества, как мы увидим позже:
const tru = (thn, _ ) => thn,
fls = (_ , els) => els;
truэто функция с двумя параметрами, которая просто игнорирует второй аргумент и возвращает первый. flsтакже является функцией с двумя параметрами, которая просто игнорирует свой первый аргумент и возвращает второй.
Почему мы кодировали truи flsтак? Ну, таким образом, две функции не только представляют две концепции trueи false, нет, они также представляют концепцию «выбора», другими словами, они также являются if/ then/ elseвыражением! Мы оцениваем ifусловие и передаем ему thenблок и elseблок в качестве аргументов. Если условие оценивается как tru, оно возвращает thenблок, если оно оценивает fls, оно возвращает elseблок. Вот пример:
tru(23, 42);
// => 23
Это возвращается 23, и это:
fls(23, 42);
// => 42
возвращается 42, как и следовало ожидать.
Есть морщина, однако:
tru(console.log("then branch"), console.log("else branch"));
// then branch
// else branch
Это печатает оба then branch и else branch! Зачем?
Ну, он возвращает возвращаемое значение первого аргумента, но он оценивает оба аргумента, поскольку ECMAScript является строгим и всегда оценивает все аргументы функции перед вызовом функции. IOW: он оценивает первый аргумент console.log("then branch"), который просто возвращает, undefinedи имеет побочный эффект печати then branchна консоль, и он оценивает второй аргумент, который также возвращает undefinedи печатает на консоль как побочный эффект. Затем возвращается первое undefined.
В λ-исчислении, где было изобретено это кодирование, это не проблема: λ-исчисление чисто , что означает, что оно не имеет побочных эффектов; поэтому вы никогда не заметите, что второй аргумент также оценивается. Кроме того, λ-исчисление является ленивым (или, по крайней мере, его часто оценивают в обычном порядке), то есть фактически не оценивает аргументы, которые не нужны. Итак, IOW: в λ-исчислении второй аргумент никогда не будет оцениваться, и если бы это было так, мы бы этого не заметили.
ECMAScript, однако, является строгим , то есть он всегда оценивает все аргументы. Ну, на самом деле, не всегда: if/ then/ else, например, только оценивает thenветвь, если условие есть, trueи только оценивает elseветвь, если условие есть false. И мы хотим повторить это поведение с нашими iff. К счастью, несмотря на то, что ECMAScript не ленив, у него есть способ отложить оценку фрагмента кода, точно так же, как это делает почти любой другой язык: обернуть его в функцию, и если вы никогда не вызовете эту функцию, код будет никогда не будет казнен.
Итак, мы заключаем оба блока в функцию, и в конце вызываем возвращаемую функцию:
tru(() => console.log("then branch"), () => console.log("else branch"))();
// then branch
печатает then branchи
fls(() => console.log("then branch"), () => console.log("else branch"))();
// else branch
отпечатки else branch.
Мы могли бы реализовать традиционный if/ then/ elseтаким образом:
const iff = (cnd, thn, els) => cnd(thn, els);
iff(tru, 23, 42);
// => 23
iff(fls, 23, 42);
// => 42
Опять же, нам нужна дополнительная упаковка функций при вызове iffфункции и дополнительные скобки вызова функции в определении iff, по той же причине, что и выше:
const iff = (cnd, thn, els) => cnd(thn, els)();
iff(tru, () => console.log("then branch"), () => console.log("else branch"));
// then branch
iff(fls, () => console.log("then branch"), () => console.log("else branch"));
// else branch
Теперь, когда у нас есть эти два определения, мы можем их реализовать or. Сначала мы смотрим на таблицу истинности or: если первый операнд является правдивым, то результат выражения совпадает с первым операндом. В противном случае, результат выражения является результатом второго операнда. Вкратце: если первый операнд true, мы возвращаем первый операнд, в противном случае мы возвращаем второй операнд:
const orr = (a, b) => iff(a, () => a, () => b);
Давайте проверим, что это работает:
orr(tru,tru);
// => tru(thn, _) {}
orr(tru,fls);
// => tru(thn, _) {}
orr(fls,tru);
// => tru(thn, _) {}
orr(fls,fls);
// => fls(_, els) {}
Большой! Однако это определение выглядит немного некрасиво. Помните, truи flsуже сами по себе действуйте как условные, так что на самом деле в этом нет необходимости iff, и, следовательно, все эти функции оборачиваются вообще:
const orr = (a, b) => a(a, b);
Там у вас есть это: or(плюс другие логические операторы) определены только с определениями функций и вызовами функций всего за несколько строк:
const tru = (thn, _ ) => thn,
fls = (_ , els) => els,
orr = (a , b ) => a(a, b),
nnd = (a , b ) => a(b, a),
ntt = a => a(fls, tru),
xor = (a , b ) => a(ntt(b), b),
iff = (cnd, thn, els) => cnd(thn, els)();
К сожалению, эта реализация довольно бесполезна: в ECMAScript нет функций или операторов, которые возвращают truили fls, все они возвращают trueили false, поэтому мы не можем использовать их с нашими функциями. Но мы все еще можем многое сделать. Например, это реализация односвязного списка:
const cons = (hd, tl) => which => which(hd, tl),
car = l => l(tru),
cdr = l => l(fls);
Объекты (Скала)
Возможно, вы заметили что-то своеобразное: truи, flsиграя двойную роль, они действуют как значения данных trueи false, в то же время, они также действуют как условное выражение. Это данные и поведение , объединенные в один ... хм ... "предмет" ... или (смею сказать) объект !
Действительно, truи flsесть объекты. И, если вы когда-либо использовали Smalltalk, Self, Newspeak или другие объектно-ориентированные языки, вы заметите, что они реализуют булевы значения точно таким же образом. Я продемонстрирую такую реализацию здесь, в Scala:
sealed abstract trait Buul {
def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): T
def &&&(other: ⇒ Buul): Buul
def |||(other: ⇒ Buul): Buul
def ntt: Buul
}
case object Tru extends Buul {
override def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): U = thn
override def &&&(other: ⇒ Buul) = other
override def |||(other: ⇒ Buul): this.type = this
override def ntt = Fls
}
case object Fls extends Buul {
override def apply[T, U <: T, V <: T](thn: ⇒ U)(els: ⇒ V): V = els
override def &&&(other: ⇒ Buul): this.type = this
override def |||(other: ⇒ Buul) = other
override def ntt = Tru
}
object BuulExtension {
import scala.language.implicitConversions
implicit def boolean2Buul(b: ⇒ Boolean) = if (b) Tru else Fls
}
import BuulExtension._
(2 < 3) { println("2 is less than 3") } { println("2 is greater than 3") }
// 2 is less than 3
Вот почему, кстати, всегда работает функция «Заменить условное на полиморфное рефакторинг»: вы всегда можете заменить любое условное выражение в вашей программе полиморфной отправкой сообщений, потому что, как мы только что показали, полиморфная диспетчеризация сообщений может заменить условные выражения, просто внедрив их. Такие языки, как Smalltalk, Self и Newspeak, являются доказательством существования, потому что у этих языков нет даже условных выражений. (У них также нет циклов, BTW или вообще каких- либо встроенных в язык структур управления, кроме полиморфной отправки сообщений, называемых вызовами виртуальных методов.)
Pattern Matching (Haskell)
Вы также можете определить, orиспользуя сопоставление с образцом или что-то вроде определения частичной функции Haskell:
True ||| _ = True
_ ||| b = b
Конечно, сопоставление с образцом является формой условного выполнения, но опять же, как и объектно-ориентированная отправка сообщений.