Funktionen (ECMAScript)
Sie benötigen lediglich Funktionsdefinitionen und Funktionsaufrufe. Sie benötigen keine Verzweigungen, Bedingungen, Operatoren oder eingebauten Funktionen. Ich werde eine Implementierung mit ECMAScript demonstrieren.
Definieren wir zunächst zwei Funktionen namens true
und false
. Wir können sie so definieren, wie wir wollen, sie sind völlig willkürlich, aber wir werden sie auf eine ganz besondere Art definieren, die einige Vorteile hat, wie wir später sehen werden:
const tru = (thn, _ ) => thn,
fls = (_ , els) => els;
tru
ist eine Funktion mit zwei Parametern, die das zweite Argument einfach ignoriert und das erste zurückgibt. fls
ist auch eine Funktion mit zwei Parametern, die das erste Argument einfach ignoriert und das zweite zurückgibt.
Warum haben wir tru
und auf fls
diese Weise codiert ? Nun, auf diese Weise, die beiden Funktionen nicht nur repräsentieren die beiden Begriffe true
und false
, nein, zugleich, sie auch das Konzept der „Wahl“, mit anderen Worten darstellen, sind sie auch ein if
/ then
/ else
Ausdruck! Wir werten die if
Bedingung aus und übergeben sie als Argumente an den then
Block und den else
Block. Wenn die Bedingung den Wert "0" ergibt tru
, wird der then
Block zurückgegeben. Wenn die Bedingung den Wert "0" ergibt fls
, wird der else
Block zurückgegeben. Hier ist ein Beispiel:
tru(23, 42);
// => 23
Dies kehrt zurück 23
und dies:
fls(23, 42);
// => 42
kehrt zurück 42
, so wie Sie es erwarten würden.
Es gibt jedoch eine Falte:
tru(console.log("then branch"), console.log("else branch"));
// then branch
// else branch
Dies druckt sowohl then branch
als auch else branch
! Warum?
Nun, es gibt den Rückgabewert des ersten Arguments zurück, wertet jedoch beide Argumente aus, da ECMAScript streng ist und immer alle Argumente für eine Funktion auswertet, bevor die Funktion aufgerufen wird. IOW: Es wertet das erste Argument aus console.log("then branch")
, das einfach zurückgibt undefined
und den Nebeneffekt des Druckens then branch
auf die Konsole hat, und es wertet das zweite Argument aus, das ebenfalls undefined
als Nebeneffekt zurückgibt und auf die Konsole druckt. Dann wird der erste zurückgegeben undefined
.
In λ-Kalkül, wo diese Kodierung erfunden wurde, ist das kein Problem: λ-Kalkül ist rein , was bedeutet, dass es keine Nebenwirkungen hat; daher würde man nie bemerken, dass das zweite Argument auch ausgewertet wird. Außerdem ist λ-Kalkül faul (oder zumindest wird es oft in normaler Reihenfolge ausgewertet), was bedeutet, dass es keine Argumente auswertet, die nicht benötigt werden. Also, IOW: In der λ-Rechnung würde das zweite Argument niemals ausgewertet, und wenn es so wäre, würden wir es nicht bemerken.
ECMAScript ist jedoch streng , dh es wertet immer alle Argumente aus. Nun, eigentlich nicht immer: der if
/ then
/ else
zum Beispiel wertet nur den then
Zweig , wenn die Bedingung ist true
und wertet nur den else
Zweig , wenn die Bedingung ist false
. Und dieses Verhalten wollen wir mit unserem replizieren iff
. Glücklicherweise kann ECMAScript, obwohl es nicht faul ist, die Auswertung eines Codeteils verzögern, so wie es fast jede andere Sprache tut: Es wird in eine Funktion eingebunden, und wenn Sie diese Funktion nie aufrufen, wird der Code dies tun nie hingerichtet werden.
Wir binden also beide Blöcke in eine Funktion ein und rufen am Ende die zurückgegebene Funktion auf:
tru(() => console.log("then branch"), () => console.log("else branch"))();
// then branch
druckt then branch
und
fls(() => console.log("then branch"), () => console.log("else branch"))();
// else branch
druckt else branch
.
Wir konnten die traditionelle implementieren if
/ then
/ auf else
diese Weise:
const iff = (cnd, thn, els) => cnd(thn, els);
iff(tru, 23, 42);
// => 23
iff(fls, 23, 42);
// => 42
Auch hier benötigen wir einen zusätzlichen Funktionsumbruch beim Aufrufen der iff
Funktion und der zusätzlichen Funktionsaufrufklammern in der Definition von iff
, und zwar aus dem gleichen Grund wie oben:
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
Nachdem wir diese beiden Definitionen haben, können wir sie implementieren or
. Zunächst untersuchen wir die Wahrheitstabelle nach or
: Wenn der erste Operand wahr ist, ist das Ergebnis des Ausdrucks dasselbe wie der erste Operand. Ansonsten ist das Ergebnis des Ausdrucks das Ergebnis des zweiten Operanden. Kurz gesagt: Wenn der erste Operand ist true
, geben wir den ersten Operanden zurück, andernfalls geben wir den zweiten Operanden zurück:
const orr = (a, b) => iff(a, () => a, () => b);
Lassen Sie uns herausfinden, dass es funktioniert:
orr(tru,tru);
// => tru(thn, _) {}
orr(tru,fls);
// => tru(thn, _) {}
orr(fls,tru);
// => tru(thn, _) {}
orr(fls,fls);
// => fls(_, els) {}
Groß! Diese Definition sieht jedoch etwas hässlich aus. Denken Sie daran, tru
und fls
handeln Sie bereits wie eine Bedingung für sich, so dass wirklich keine Notwendigkeit besteht iff
, und daher all diese Funktionen überhaupt zu umschließen:
const orr = (a, b) => a(a, b);
Da haben Sie es: or
(plus andere boolesche Operatoren) definiert mit nichts als Funktionsdefinitionen und Funktionsaufrufen in nur wenigen Zeilen:
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)();
Leider ist diese Implementierung ziemlich nutzlos: Es gibt keine Funktionen oder Operatoren in ECMAScript, die zurückgeben tru
oder fls
alle zurückgeben true
oder false
, daher können wir sie nicht mit unseren Funktionen verwenden. Aber wir können noch viel tun. Dies ist beispielsweise eine Implementierung einer einfach verknüpften Liste:
const cons = (hd, tl) => which => which(hd, tl),
car = l => l(tru),
cdr = l => l(fls);
Gegenstände (Scala)
Sie haben vielleicht bemerkt etwas Besonderes: tru
und fls
eine doppelte Rolle spielen, wirken sie sowohl als die Datenwerte true
und false
, aber zur gleichen Zeit, sie auch als bedingter Ausdruck handeln. Sie sind Daten und Verhalten , gebündelt in einem ... ähm ... "Ding" ... oder (wage ich zu sagen) Objekt !
In der Tat tru
und fls
sind Objekte. Und wenn Sie jemals Smalltalk, Self, Newspeak oder andere objektorientierte Sprachen verwendet haben, werden Sie feststellen, dass sie Boolesche Werte genauso implementieren. Ich werde eine solche Implementierung hier in Scala demonstrieren:
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
Dies ist übrigens der Grund, warum das Refactoring "Bedingt durch Polymorphismus ersetzen" immer funktioniert: Sie können immer alle Bedingungen in Ihrem Programm durch polymorphen Nachrichtenversand ersetzen, da der polymorphe Nachrichtenversand, wie wir gerade gezeigt haben, Bedingungen durch einfaches Implementieren ersetzen kann. Sprachen wie Smalltalk, Self und Newspeak sind der Existenzbeweis dafür, weil diese Sprachen nicht einmal Bedingungen haben. (Sie haben auch keine Schleifen, BTW oder wirklich irgendeine Art von in die Sprache eingebauten Kontrollstrukturen, außer für den polymorphen Nachrichtenversand, auch bekannt als virtuelle Methodenaufrufe.)
Mustervergleich (Haskell)
Sie können auch or
mithilfe des Mustervergleichs oder so etwas wie den Definitionen der Teilfunktionen von Haskell definieren:
True ||| _ = True
_ ||| b = b
Natürlich ist der Mustervergleich eine Form der bedingten Ausführung, aber auch der objektorientierte Nachrichtenversand.