В вашем вопросе есть небольшое заблуждение. В функциональных языках if
не обязательно является функцией трех параметров. Скорее, иногда это две функции двух параметров.
В частности, именно так работает церковное кодирование логических значений в λ-исчислении: есть две функциидавайте назовем их True
и False
. Обе функции имеют два параметра. True
просто возвращает первый аргумент, False
просто возвращает второй аргумент.
Сначала давайте определим две функции с именами true
и false
. Мы можем определить их так, как хотим, они совершенно произвольны, но мы определим их совершенно особым образом, что имеет некоторые преимущества, как мы увидим позже (я буду использовать ECMAScript как несколько разумное приближение λ-исчисления, которое, вероятно,может быть прочитано большей частью посетителей этого сайта, чем само λ-исчисление):
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
.
В λ-исчислении, где было изобретено это кодирование, это не проблема: λ-исчисление равно pure , что означает, что оно неиметь какие-либо побочные эффекты;поэтому вы никогда не заметите, что второй аргумент также оценивается. Кроме того, λ-исчисление является ленивым (или, по крайней мере, оно часто оценивается в обычном порядке), то есть фактически не оценивает аргументы, которые не нужны. Итак, 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 или другие чисто объектно-ориентированные языки, вы заметите, что они реализуют логические значения абсолютно одинаково: два объекта true
и false
, которые имеют метод с именем if
который принимает два блока (функции, лямбды и т. д.) в качестве аргументов и оценивает один из них.
Вот пример того, как это может выглядеть в 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 на PLANNER Карла Хьюитта; актеры на основе Карла Хьюитта на Smalltalk Алана Кея), я не удивлюсь, если этооказался шагом в правильном направлении, чтобы решить вашу проблему.