Funzioni (ECMAScript)
Tutto ciò che serve sono le definizioni delle funzioni e le chiamate di funzione. Non sono necessarie diramazioni, condizionali, operatori o funzioni incorporate. Dimostrerò un'implementazione usando ECMAScript.
Innanzitutto, definiamo due funzioni chiamate true
e false
. Potremmo definirli come vogliamo, sono completamente arbitrari, ma li definiremo in un modo molto speciale che presenta alcuni vantaggi, come vedremo più avanti:
const tru = (thn, _ ) => thn,
fls = (_ , els) => els;
tru
è una funzione con due parametri che semplicemente ignora il suo secondo argomento e restituisce il primo. fls
è anche una funzione con due parametri che semplicemente ignora il suo primo argomento e restituisce il secondo.
Perché abbiamo codificato tru
e in fls
questo modo? Bene, in questo modo, le due funzioni non solo rappresentano i due concetti di true
e false
, no, allo stesso tempo rappresentano anche il concetto di "scelta", in altre parole, sono anche if
/ then
/ else
espressione! Valutiamo la if
condizione e passiamo il then
blocco e il else
blocco come argomenti. Se la condizione viene valutata tru
, verrà restituito il then
blocco, se viene valutato fls
, restituirà il else
blocco. Ecco un esempio:
tru(23, 42);
// => 23
Questo ritorna 23
e questo:
fls(23, 42);
// => 42
ritorna 42
, proprio come ti aspetteresti.
C'è una ruga, tuttavia:
tru(console.log("then branch"), console.log("else branch"));
// then branch
// else branch
Questo stampa sia then branch
e else branch
! Perché?
Bene, restituisce il valore di ritorno del primo argomento, ma valuta entrambi gli argomenti, poiché ECMAScript è rigoroso e valuta sempre tutti gli argomenti in una funzione prima di chiamare la funzione. IOW: valuta il primo argomento che è console.log("then branch")
, che semplicemente ritorna undefined
e ha l'effetto collaterale della stampa then branch
sulla console, e valuta il secondo argomento, che ritorna undefined
e stampa anche sulla console come effetto collaterale. Quindi restituisce il primo undefined
.
Nel calcolo λ, dove è stata inventata questa codifica, non è un problema: il calcolo λ è puro , il che significa che non ha effetti collaterali; pertanto non noteresti mai che anche il secondo argomento viene valutato. Inoltre, λ-calculus è pigro (o almeno, viene spesso valutato in ordine normale), il che significa che in realtà non valuta argomenti che non sono necessari. Quindi, IOW: nel calcolo λ il secondo argomento non verrebbe mai valutato, e se lo fosse, non lo noteremmo.
ECMAScript, tuttavia, è rigoroso , ovvero valuta sempre tutti gli argomenti. Bene, in realtà, non sempre: if
/ then
/ else
, ad esempio, valuta il then
ramo solo se la condizione è true
e valuta il else
ramo solo se la condizione è false
. E vogliamo replicare questo comportamento con il nostro iff
. Per fortuna, anche se ECMAScript non è pigro, ha un modo per ritardare la valutazione di un pezzo di codice, allo stesso modo quasi tutte le altre lingue: racchiudilo in una funzione, e se non la chiami mai, il codice non essere mai eseguito.
Quindi, avvolgiamo entrambi i blocchi in una funzione e alla fine chiamiamo la funzione che viene restituita:
tru(() => console.log("then branch"), () => console.log("else branch"))();
// then branch
stampe then branch
e
fls(() => console.log("then branch"), () => console.log("else branch"))();
// else branch
stampe else branch
.
Potremmo implementare il tradizionale if
/ then
/ in else
questo modo:
const iff = (cnd, thn, els) => cnd(thn, els);
iff(tru, 23, 42);
// => 23
iff(fls, 23, 42);
// => 42
Ancora una volta, abbiamo bisogno di alcune funzioni extra quando si chiama la iff
funzione e la funzione extra chiama parentesi nella definizione di iff
, per lo stesso motivo di cui sopra:
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
Ora che abbiamo queste due definizioni, possiamo implementare or
. Innanzitutto, or
esaminiamo la tabella della verità per : se il primo operando è vero, allora il risultato dell'espressione è lo stesso del primo operando. Altrimenti, il risultato dell'espressione è il risultato del secondo operando. In breve: se il primo operando è true
, restituiamo il primo operando, altrimenti restituiamo il secondo operando:
const orr = (a, b) => iff(a, () => a, () => b);
Diamo un'occhiata che funziona:
orr(tru,tru);
// => tru(thn, _) {}
orr(tru,fls);
// => tru(thn, _) {}
orr(fls,tru);
// => tru(thn, _) {}
orr(fls,fls);
// => fls(_, els) {}
Grande! Tuttavia, questa definizione sembra un po 'brutta. Ricorda, tru
e fls
già agisci come un condizionale da solo, quindi davvero non ce n'è bisogno iff
, e quindi tutta quella funzione che si avvolge affatto:
const orr = (a, b) => a(a, b);
Ecco qua: or
(oltre ad altri operatori booleani) definiti con nient'altro che definizioni di funzione e chiamate di funzione in poche righe:
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)();
Sfortunatamente, questa implementazione è piuttosto inutile: non ci sono funzioni o operatori in ECMAScript che restituiscono tru
o fls
, tutti ritornano true
o false
, quindi non possiamo usarli con le nostre funzioni. Ma c'è ancora molto che possiamo fare. Ad esempio, questa è un'implementazione di un elenco collegato singolarmente:
const cons = (hd, tl) => which => which(hd, tl),
car = l => l(tru),
cdr = l => l(fls);
Oggetti (Scala)
Potresti aver notato qualcosa di strano: tru
e fls
svolgono un doppio ruolo, agiscono sia come valori di dati true
che false
, ma allo stesso tempo, fungono anche da espressione condizionale. Sono dati e comportamenti , raggruppati in un ... uhm ... "cosa" ... o (oserei dire) oggetto !
Anzi, tru
e fls
sono oggetti. E, se hai mai usato Smalltalk, Self, Newspeak o altri linguaggi orientati agli oggetti, avrai notato che implementano i booleani esattamente allo stesso modo. Dimostrerò una tale implementazione qui a 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
Questo BTW è il motivo per cui Sostituisci condizionale con polimorfismo Il refactoring funziona sempre: puoi sempre sostituire qualsiasi condizionale nel tuo programma con l'invio di messaggi polimorfici, perché come abbiamo appena mostrato, l'invio di messaggi polimorfici può sostituire i condizionali semplicemente implementandoli. Lingue come Smalltalk, Self e Newspeak ne sono la prova dell'esistenza, perché quelle lingue non hanno nemmeno i condizionali. (Inoltre, non dispongono di loop, BTW o di alcun tipo di strutture di controllo integrate nel linguaggio, ad eccezione dell'invio di messaggi polimorfici, ovvero chiamate di metodo virtuali.)
Pattern Matching (Haskell)
Puoi anche definire or
usando la corrispondenza del modello, o qualcosa come le definizioni di funzione parziale di Haskell:
True ||| _ = True
_ ||| b = b
Naturalmente, la corrispondenza dei modelli è una forma di esecuzione condizionale, ma anche in questo caso lo è anche l'invio di messaggi orientato agli oggetti.