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 truey 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;
trues una función con dos parámetros que simplemente ignora su segundo argumento y devuelve el primero. flsTambién es una función con dos parámetros que simplemente ignora su primer argumento y devuelve el segundo.
¿Por qué codificamos truy de flsesta manera? Bueno, de esta manera, las dos funciones no solo representan los dos conceptos truey 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 ifcondición y le pasamos el thenbloque y el elsebloque como argumentos. Si la condición se evalúa como tru, devolverá el thenbloque, si se evalúa como fls, devolverá el elsebloque. 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 undefinedy tiene el efecto secundario de imprimir then branchen la consola, y evalúa el segundo argumento, que también regresa undefinede 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 thenrama si la condición es truey solo evalúa la elserama 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 branchy
fls(() => console.log("then branch"), () => console.log("else branch"))();
// else branch
impresiones else branch.
Podríamos implementar el if/ then/ tradicional de elseesta 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 ifffunció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, truy flsya actúa como un condicional por sí mismo, por lo que realmente no es necesario iffy, 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 truo fls, todos regresan trueo 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: truy flsdesempeñan un doble papel, actúan como valores de datos truey 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, truy flsson 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 oruso 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.