Функції (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
Звичайно, відповідність шаблону є формою умовного виконання, але знову ж таки, так само об'єктно-орієнтована відправка повідомлень.