Funciones (ECMAScript)
Todo lo que necesita son definiciones de funciones y llamadas a funciones. No necesita ninguna ramificación, condicionales, operadores o funciones integradas. Demostraré una implementación usando ECMAScript.
Primero, definamos dos funciones llamadas true
y false
. Podríamos definirlos de la forma que queramos, son completamente arbitrarios, pero los definiremos de una manera muy especial que tiene algunas ventajas como veremos más adelante:
const tru = (thn, _ ) => thn,
fls = (_ , els) => els;
tru
es una función con dos parámetros que simplemente ignora su segundo argumento y devuelve el primero. fls
También es una función con dos parámetros que simplemente ignora su primer argumento y devuelve el segundo.
¿Por qué codificamos tru
y de fls
esta manera? Bueno, de esta manera, las dos funciones no solo representan los dos conceptos true
y false
, no, al mismo tiempo, también representan el concepto de "elección", en otras palabras, ¡también son una expresión if
/ then
/ else
! Evaluamos la if
condición y le pasamos el then
bloque y el else
bloque como argumentos. Si la condición se evalúa como tru
, devolverá el then
bloque, si se evalúa como fls
, devolverá el else
bloque. Aquí hay un ejemplo:
tru(23, 42);
// => 23
Esto vuelve 23
, y esto:
fls(23, 42);
// => 42
vuelve 42
, tal como es de esperar.
Sin embargo, hay una arruga:
tru(console.log("then branch"), console.log("else branch"));
// then branch
// else branch
Esto imprime ambos then branch
y else branch
! ¿Por qué?
Bueno, devuelve el valor de retorno del primer argumento, pero evalúa ambos argumentos, ya que ECMAScript es estricto y siempre evalúa todos los argumentos de una función antes de llamar a la función. IOW: evalúa el primer argumento que es console.log("then branch")
, que simplemente regresa undefined
y tiene el efecto secundario de imprimir then branch
en la consola, y evalúa el segundo argumento, que también regresa undefined
e imprime en la consola como un efecto secundario. Luego, devuelve el primero undefined
.
En el cálculo λ, donde se inventó esta codificación, eso no es un problema: el cálculo λ es puro , lo que significa que no tiene efectos secundarios; por lo tanto, nunca notarías que el segundo argumento también se evalúa. Además, el cálculo λ es perezoso (o al menos, a menudo se evalúa en orden normal), lo que significa que en realidad no evalúa argumentos que no son necesarios. Entonces, IOW: en el cálculo λ, el segundo argumento nunca sería evaluado, y si lo fuera, no lo notaríamos.
ECMAScript, sin embargo, es estricto , es decir, siempre evalúa todos los argumentos. Bueno, en realidad, no siempre: el if
/ then
/ else
, por ejemplo, solo evalúa la then
rama si la condición es true
y solo evalúa la else
rama si la condición es false
. Y queremos replicar este comportamiento con nuestro iff
. Afortunadamente, a pesar de que ECMAScript no es perezoso, tiene una forma de retrasar la evaluación de un fragmento de código, de la misma manera que lo hace casi cualquier otro idioma: envolverlo en una función, y si nunca llama a esa función, el código lo hará Nunca ser ejecutado.
Entonces, envolvemos ambos bloques en una función, y al final llamamos a la función que se devuelve:
tru(() => console.log("then branch"), () => console.log("else branch"))();
// then branch
impresiones then branch
y
fls(() => console.log("then branch"), () => console.log("else branch"))();
// else branch
impresiones else branch
.
Podríamos implementar el if
/ then
/ tradicional de else
esta manera:
const iff = (cnd, thn, els) => cnd(thn, els);
iff(tru, 23, 42);
// => 23
iff(fls, 23, 42);
// => 42
Nuevamente, necesitamos un ajuste de función adicional al llamar a la iff
función y los paréntesis de llamada de función adicional en la definición de iff
, por la misma razón que arriba:
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
Ahora que tenemos esas dos definiciones, podemos implementar or
. Primero, miramos la tabla de verdad para or
: si el primer operando es verdadero, entonces el resultado de la expresión es el mismo que el primer operando. De lo contrario, el resultado de la expresión es el resultado del segundo operando. En resumen: si el primer operando es true
, devolvemos el primer operando, de lo contrario devolvemos el segundo operando:
const orr = (a, b) => iff(a, () => a, () => b);
Veamos que funciona:
orr(tru,tru);
// => tru(thn, _) {}
orr(tru,fls);
// => tru(thn, _) {}
orr(fls,tru);
// => tru(thn, _) {}
orr(fls,fls);
// => fls(_, els) {}
¡Excelente! Sin embargo, esa definición se ve un poco fea. Recuerde, tru
y fls
ya actúa como un condicional por sí mismo, por lo que realmente no es necesario iff
y, por lo tanto, toda esa función se ajusta:
const orr = (a, b) => a(a, b);
Ahí lo tiene: or
(más otros operadores booleanos) definidos con nada más que definiciones de funciones y llamadas de funciones en solo un puñado de líneas:
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)();
Desafortunadamente, esta implementación es bastante inútil: no hay funciones u operadores en ECMAScript que regresen tru
o fls
, todos regresan true
o false
, por lo que no podemos usarlos con nuestras funciones. Pero todavía hay mucho que podemos hacer. Por ejemplo, esta es una implementación de una lista vinculada individualmente:
const cons = (hd, tl) => which => which(hd, tl),
car = l => l(tru),
cdr = l => l(fls);
Objetos (Scala)
Es posible que haya notado algo peculiar: tru
y fls
desempeñan un doble papel, actúan como valores de datos true
y false
, al mismo tiempo, también actúan como una expresión condicional. Son datos y comportamiento , agrupados en un ... uhm ... "cosa" ... ¡u (me atrevo a decir) objeto !
De hecho, tru
y fls
son objetos. Y, si alguna vez ha utilizado Smalltalk, Self, Newspeak u otros lenguajes orientados a objetos, habrá notado que implementan booleanos exactamente de la misma manera. Demostraré tal implementación aquí en 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
Por cierto, la refactorización Reemplazar condicional por polimorfismo siempre funciona: siempre puede reemplazar todos y cada uno de los condicionales en su programa con envío de mensajes polimórficos, porque como acabamos de mostrar, el envío de mensajes polimórficos puede reemplazar condicionales simplemente implementándolos. Idiomas como Smalltalk, Self y Newspeak son prueba de la existencia de eso, porque esos idiomas ni siquiera tienen condicionales. (Tampoco tienen bucles, por cierto, o realmente ningún tipo de estructuras de control incorporadas en el lenguaje, excepto el envío de mensajes polimórficos, también llamadas de método virtual).
Coincidencia de patrones (Haskell)
También podría definir el or
uso de la coincidencia de patrones, o algo así como las definiciones de funciones parciales de Haskell:
True ||| _ = True
_ ||| b = b
Por supuesto, la coincidencia de patrones es una forma de ejecución condicional, pero, de nuevo, también lo es el envío de mensajes orientado a objetos.