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 true
e 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. fls
também é uma função com dois parâmetros que simplesmente ignora seu primeiro argumento e retorna o segundo.
Por que codificamos tru
e fls
dessa maneira? Bem, assim, as duas funções não apenas representam os dois conceitos de true
e 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 if
condição e passamos o then
bloco e o else
bloco como argumentos. Se a condição for avaliada como tru
, ela retornará o then
bloco; se avaliar fls
, retornará o else
bloco. 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 undefined
e tem o efeito colateral de impressão then branch
no console, e avalia o segundo argumento, que também retorna undefined
e 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 then
ramificação se a condição for true
e só avalia a else
ramificaçã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 branch
e
fls(() => console.log("then branch"), () => console.log("else branch"))();
// else branch
impressões else branch
.
Poderíamos implementar o tradicional if
/ then
/ else
desta 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 iff
funçã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, tru
e fls
já agem como condicionais sozinhos; portanto, realmente não há necessidade iff
e, 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 tru
ou fls
, todos retornam true
ou 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: tru
e fls
desempenha um papel duplo, eles agem como valores dos dados true
e 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, tru
e fls
sã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 or
uso 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.