Funkcje (ECMAScript)
Potrzebujesz tylko definicji funkcji i wywołań funkcji. Nie potrzebujesz żadnych rozgałęzień, warunków, operatorów ani wbudowanych funkcji. Zademonstruję implementację przy użyciu ECMAScript.
Najpierw zdefiniujmy dwie funkcje o nazwie true
i false
. Możemy zdefiniować je w dowolny sposób, są one całkowicie arbitralne, ale zdefiniujemy je w bardzo specjalny sposób, który ma pewne zalety, co zobaczymy później:
const tru = (thn, _ ) => thn,
fls = (_ , els) => els;
tru
jest funkcją z dwoma parametrami, która po prostu ignoruje swój drugi argument i zwraca pierwszy. fls
jest także funkcją z dwoma parametrami, która po prostu ignoruje swój pierwszy argument i zwraca drugi.
Dlaczego kodować tru
i fls
w ten sposób? Cóż, w ten sposób dwie funkcje reprezentują nie tylko dwie koncepcje true
i false
, nie, jednocześnie reprezentują także koncepcję „wyboru”, innymi słowy, są również wyrażeniem if
/ then
/ else
! Oceniamy if
warunek i przekazujemy then
blok i else
blok jako argumenty. Jeśli warunek oceni się na tru
, zwróci then
blok, jeśli oceni fls
, to zwróci else
blok. Oto przykład:
tru(23, 42);
// => 23
Zwraca 23
i to:
fls(23, 42);
// => 42
zwraca 42
, tak jak można się spodziewać.
Jest jednak zmarszczka:
tru(console.log("then branch"), console.log("else branch"));
// then branch
// else branch
To drukuje zarówno then branch
i else branch
! Czemu?
Cóż, to zwraca wartość zwracaną pierwszego argumentu, ale ocenia oba argumenty, ponieważ ECMAScript jest surowe i zawsze ocenia wszystkie argumenty do funkcji przed wywołaniem funkcji. IOW: ocenia pierwszy argument console.log("then branch")
, który po prostu zwraca undefined
i wywołuje efekt uboczny drukowania then branch
na konsoli, i ocenia drugi argument, który również zwraca undefined
i drukuje na konsoli jako efekt uboczny. Następnie zwraca pierwszy undefined
.
W rachunku λ, w którym wymyślono to kodowanie, nie stanowi to problemu: rachunek λ jest czysty , co oznacza, że nie ma żadnych skutków ubocznych; dlatego nigdy nie zauważysz, że drugi argument również jest oceniany. Ponadto rachunek λ jest leniwy (a przynajmniej często jest oceniany w normalnej kolejności), co oznacza, że tak naprawdę nie ocenia argumentów, które nie są potrzebne. IOW: w rachunku λ drugi argument nigdy nie byłby oceniany, a gdyby tak było, nie zauważylibyśmy.
ECMAScript jest jednak ścisły , tzn. Zawsze ocenia wszystkie argumenty. Właściwie nie zawsze: na przykład if
/ then
/ else
ocenia then
gałąź tylko wtedy, gdy jest spełniony warunek true
i ocenia else
gałąź tylko wtedy, gdy jest spełniony warunek false
. I chcemy powtórzyć to zachowanie z naszym iff
. Na szczęście, mimo że ECMAScript nie jest leniwy, ma sposób na opóźnienie oceny fragmentu kodu, tak samo jak prawie każdy inny język: zawija go w funkcję, a jeśli nigdy nie wywołasz tej funkcji, kod będzie nigdy nie zostanie stracony.
Tak więc zawijamy oba bloki w funkcję, a na końcu wywołuje zwracaną funkcję:
tru(() => console.log("then branch"), () => console.log("else branch"))();
// then branch
wydruki then branch
i
fls(() => console.log("then branch"), () => console.log("else branch"))();
// else branch
odbitki else branch
.
Możemy wdrożyć tradycyjny if
/ then
/ w else
ten sposób:
const iff = (cnd, thn, els) => cnd(thn, els);
iff(tru, 23, 42);
// => 23
iff(fls, 23, 42);
// => 42
Ponownie potrzebujemy dodatkowego zawijania funkcji podczas wywoływania iff
funkcji i dodatkowych nawiasów wywoływania funkcji w definicji z iff
tego samego powodu, co powyżej:
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
Teraz, gdy mamy te dwie definicje, możemy je wdrożyć or
. Najpierw przyjrzymy się tabeli prawdy dla or
: jeśli pierwszy operand jest prawdziwy, wynik wyrażenia jest taki sam jak pierwszy operand. W przeciwnym razie wynik wyrażenia jest wynikiem drugiego operandu. W skrócie: jeśli pierwszym operandem jest true
, zwracamy pierwszy operand, w przeciwnym razie zwracamy drugi operand:
const orr = (a, b) => iff(a, () => a, () => b);
Sprawdźmy, czy to działa:
orr(tru,tru);
// => tru(thn, _) {}
orr(tru,fls);
// => tru(thn, _) {}
orr(fls,tru);
// => tru(thn, _) {}
orr(fls,fls);
// => fls(_, els) {}
Świetny! Jednak ta definicja wygląda trochę brzydko. Pamiętajcie, tru
i fls
już sami zachowują się jak warunek, więc naprawdę nie ma takiej potrzeby iff
, a zatem wszystkie te funkcje są w ogóle opakowane:
const orr = (a, b) => a(a, b);
or
Oto on: (plus inne operatory logiczne) zdefiniowane za pomocą definicji funkcji i wywołań funkcji w zaledwie kilku liniach:
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)();
Niestety, ta implementacja jest raczej bezużyteczna: w ECMAScript nie ma żadnych funkcji ani operatorów, które zwracają tru
lub fls
wszystkie zwracają true
lub false
, więc nie możemy ich używać z naszymi funkcjami. Ale wciąż możemy wiele zrobić. Na przykład jest to implementacja pojedynczo połączonej listy:
const cons = (hd, tl) => which => which(hd, tl),
car = l => l(tru),
cdr = l => l(fls);
Obiekty (Scala)
Być może zauważyliście coś dziwnego: tru
i fls
odgrywają podwójną rolę, działają zarówno jako wartości danych true
i false
, ale w tym samym czasie, ale także działać jako wyrażenie warunkowe. Są to dane i zachowanie , połączone w jeden… uhm… „rzecz”… lub (ośmielę się powiedzieć) obiekt !
Rzeczywiście, tru
i fls
są obiektami. A jeśli kiedykolwiek używałeś Smalltalk, Self, Newspeak lub innych języków zorientowanych obiektowo, zauważysz, że implementują booleany w dokładnie taki sam sposób. Zademonstruję taką implementację tutaj w Scali:
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
To BTW jest powodem, dla którego funkcja Zastąp warunkowe refaktoryzacją polimorfizmu zawsze działa: zawsze możesz zastąpić dowolny warunek w twoim programie polimorficzną wysyłką wiadomości, ponieważ, jak właśnie pokazaliśmy, polimorficzna wysyłka wiadomości może zastąpić warunki warunkowe, po prostu je wdrażając. Języki takie jak Smalltalk, Self i Newspeak są tego dowodem na istnienie, ponieważ te języki nie mają nawet warunkowych. (Nie mają też pętli, BTW ani żadnych struktur kontrolnych wbudowanych w język, z wyjątkiem polimorficznego wysyłania komunikatów, czyli wirtualnych wywołań metod.)
Dopasowywanie wzorów (Haskell)
Można również zdefiniować or
za pomocą dopasowania wzorca lub czegoś w rodzaju częściowych definicji funkcji Haskella:
True ||| _ = True
_ ||| b = b
Oczywiście dopasowanie wzorca jest formą wykonania warunkowego, ale z drugiej strony, podobnie jak obiektowe wysyłanie wiadomości.