Функції (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);
Об'єкти (Scala)
Можливо , ви помітили що - щось особливе: 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
Ця BTW є причиною того, що замінити Conditional на Polymorphism Refactoring завжди працює: ви завжди можете замінити будь-яку умовну програму на поліморфну розсилку повідомлень, оскільки, як ми нещодавно показали, поліморфна розсилка повідомлень може замінити умовні умови, просто виконавши їх. Такі мови, як Smalltalk, Self та Newspeak, є підтвердженням існування, оскільки ці мови навіть не мають умов. (Вони також не мають циклів, BTW або дійсно будь-яких мов, вбудованих керуючих структур, за винятком поліморфної розсилки повідомлень, яка називається віртуальним методом.)
Узгодження шаблону (Haskell)
Ви також можете визначити, orвикористовуючи відповідність шаблонів або щось на зразок часткових визначень Haskell:
True ||| _ = True
_ ||| b = b
Звичайно, відповідність шаблону є формою умовного виконання, але знову ж таки, так само об'єктно-орієнтована відправка повідомлень.