Funções (ECMAScript)
Tudo o que você precisa são definições de funções e chamadas de função. Você não precisa de nenhuma ramificação, condicionais, operadores ou funções internas. Vou demonstrar uma implementação usando ECMAScript.
Primeiro, vamos definir duas funções chamadas truee false. Poderíamos defini-los da maneira que quisermos, são completamente arbitrários, mas os definiremos de uma maneira muito especial, com algumas vantagens, como veremos mais adiante:
const tru = (thn, _ ) => thn,
fls = (_ , els) => els;
trué uma função com dois parâmetros que simplesmente ignora seu segundo argumento e retorna o primeiro. flstambém é uma função com dois parâmetros que simplesmente ignora seu primeiro argumento e retorna o segundo.
Por que codificamos true flsdessa maneira? Bem, assim, as duas funções não apenas representam os dois conceitos de truee false, não, ao mesmo tempo, também representam o conceito de "escolha", ou seja, são também uma expressão if/ then/ else! Avaliamos a ifcondição e passamos o thenbloco e o elsebloco como argumentos. Se a condição for avaliada como tru, ela retornará o thenbloco; se avaliar fls, retornará o elsebloco. Aqui está um exemplo:
tru(23, 42);
// => 23
Isso retorna 23, e isso:
fls(23, 42);
// => 42
retorna 42, exatamente como você esperaria.
Há uma ruga, no entanto:
tru(console.log("then branch"), console.log("else branch"));
// then branch
// else branch
Isso imprime ambos then branch e else branch! Por quê?
Bem, ele retorna o valor de retorno do primeiro argumento, mas avalia os dois argumentos, já que ECMAScript é rigoroso e sempre avalia todos os argumentos de uma função antes de chamar a função. IOW: avalia o primeiro argumento que é console.log("then branch"), que simplesmente retorna undefinede tem o efeito colateral de impressão then branchno console, e avalia o segundo argumento, que também retorna undefinede imprime no console como efeito colateral. Então, ele retorna o primeiroundefined .
No cálculo λ, onde essa codificação foi inventada, isso não é um problema: o cálculo λ é puro , o que significa que não tem efeitos colaterais; portanto, você nunca notaria que o segundo argumento também é avaliado. Além disso, o cálculo λ é preguiçoso (ou pelo menos, é frequentemente avaliado em ordem normal), ou seja, na verdade, ele não avalia argumentos que não são necessários. Então, IOW: no cálculo λ, o segundo argumento nunca seria avaliado e, se fosse, não perceberíamos.
O ECMAScript, no entanto, é rigoroso , ou seja, sempre avalia todos os argumentos. Bem, na verdade, nem sempre: o if/ then/ else, por exemplo, avalia apenas a thenramificação se a condição for truee só avalia a elseramificação se a condição for false. E queremos replicar esse comportamento com nossosiff . Felizmente, mesmo que o ECMAScript não seja preguiçoso, ele tem uma maneira de atrasar a avaliação de um pedaço de código, da mesma forma que quase todas as outras línguas: envolva-o em uma função e, se você nunca chamar essa função, o código será nunca seja executado.
Então, envolvemos os dois blocos em uma função e, no final, chamamos a função que é retornada:
tru(() => console.log("then branch"), () => console.log("else branch"))();
// then branch
impressões then branche
fls(() => console.log("then branch"), () => console.log("else branch"))();
// else branch
impressões else branch.
Poderíamos implementar o tradicional if/ then/ elsedesta maneira:
const iff = (cnd, thn, els) => cnd(thn, els);
iff(tru, 23, 42);
// => 23
iff(fls, 23, 42);
// => 42
Novamente, precisamos de um agrupamento extra de funções ao chamar a ifffunção e os parênteses da chamada de função extra na definição de iff, pelo mesmo motivo que acima:
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
Agora que temos essas duas definições, podemos implementar or. Primeiro, olhamos para a tabela verdade or: se o primeiro operando é verdadeiro, então o resultado da expressão é o mesmo que o primeiro operando. Caso contrário, o resultado da expressão é o resultado do segundo operando. Resumindo: se o primeiro operando for true, retornamos o primeiro operando, caso contrário, retornamos o segundo operando:
const orr = (a, b) => iff(a, () => a, () => b);
Vamos verificar se funciona:
orr(tru,tru);
// => tru(thn, _) {}
orr(tru,fls);
// => tru(thn, _) {}
orr(fls,tru);
// => tru(thn, _) {}
orr(fls,fls);
// => fls(_, els) {}
Ótimo! No entanto, essa definição parece um pouco feia. Lembre-se, true flsjá agem como condicionais sozinhos; portanto, realmente não há necessidade iffe, portanto, toda essa função de empacotamento:
const orr = (a, b) => a(a, b);
Aí está: or(além de outros operadores booleanos) definidos com nada além de definições de funções e chamadas de funções em apenas algumas linhas:
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)();
Infelizmente, essa implementação é bastante inútil: não há funções ou operadores no ECMAScript que retornem truou fls, todos retornam trueou false, portanto, não podemos usá-los com nossas funções. Mas ainda há muito que podemos fazer. Por exemplo, esta é uma implementação de uma lista vinculada individualmente:
const cons = (hd, tl) => which => which(hd, tl),
car = l => l(tru),
cdr = l => l(fls);
Objetos (Scala)
Você deve ter notado algo peculiar: true flsdesempenha um papel duplo, eles agem como valores dos dados truee false, ao mesmo tempo, também atuam como expressão condicional. São dados e comportamento , agrupados em um ... uhm ... "coisa" ... ou (ouso dizer) objeto !
De fato, true flssão objetos. E, se você já usou Smalltalk, Self, Newspeak ou outras linguagens orientadas a objetos, notou que elas implementam booleanos exatamente da mesma maneira. Vou demonstrar essa implementação aqui em 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 isso que a Substituição da refatoração condicional por polimorfismo sempre funciona: você sempre pode substituir todo e qualquer condicional em seu programa pelo envio polimórfico de mensagens, porque, como mostramos, o envio polimórfico de mensagens pode substituir condicionais simplesmente implementando-os. Idiomas como Smalltalk, Self e Newspeak são a prova da existência disso, porque esses idiomas nem têm condicionais. (Eles também não têm loops, BTW ou realmente qualquer tipo de estrutura de controle interna de linguagem, exceto para envio de mensagens polimórficas, também chamadas de métodos virtuais.)
Correspondência de padrões (Haskell)
Você também pode definir o oruso de correspondência de padrões ou algo como as definições de funções parciais de Haskell:
True ||| _ = True
_ ||| b = b
Obviamente, a correspondência de padrões é uma forma de execução condicional, mas também o envio de mensagens orientadas a objetos.