Функции (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
Конечно, сопоставление с образцом является формой условного выполнения, но опять же, как и объектно-ориентированная отправка сообщений.