O cálculo λ , ou cálculo lambda, é um sistema lógico baseado em funções anônimas. Por exemplo, esta é uma expressão λ:
λf.(λx.xx)(λx.f(xx))
No entanto, para os propósitos deste desafio, simplificaremos a notação:
- Altere
λ
para\
(para facilitar a digitação):\f.(\x.xx)(\x.f(xx))
- Os
.
cabeçalhos in lambda são desnecessários, portanto podemos eliminá-lo:\f(\xxx)(\xf(xx))
- Use a notação de prefixo no estilo Unlambda com
`
para aplicação em vez de escrever as duas funções juntas (para obter uma explicação completa de como fazer isso, consulte Converter entre notações de cálculo Lambda ):\f`\x`xx\x`f`xx
- Esta é a substituição mais complicada. Substitua cada variável por um número entre parênteses, com base na profundidade aninhada da variável em relação ao cabeçalho lambda ao qual ela pertence (por exemplo, use a indexação De Bruijn baseada em 0 ). Por exemplo, em
\xx
(a função de identidade), ox
no corpo seria substituído por[0]
, porque pertence ao primeiro cabeçalho (baseado em 0) encontrado ao percorrer a expressão da variável até o final;\x\y``\x`xxxy
seria convertido em\x\y``\x`[0][0][1][0]
. Agora podemos soltar as variáveis nos cabeçalhos, saindo\\``\`[0][0][1][0]
.
A lógica combinatória é basicamente um Tarpit de Turing feito a partir do cálculo λ (bem, na verdade, veio primeiro, mas isso é irrelevante aqui).
"A lógica combinatória pode ser vista como uma variante do cálculo lambda, na qual expressões lambda (representando abstração funcional) são substituídas por um conjunto limitado de combinadores, funções primitivas das quais variáveis associadas estão ausentes". 1
O tipo mais comum de lógica combinatória é o cálculo do combinador SK , que usa as seguintes primitivas:
K = λx.λy.x
S = λx.λy.λz.xz(yz)
Às vezes, um combinador I = λx.x
é adicionado, mas é redundante, pois SKK
(ou de fato SKx
para qualquer x
) é equivalente aI
.
Tudo que você precisa é K, S e aplicativo para poder codificar qualquer expressão no cálculo λ. Como exemplo, aqui está uma tradução da função λf.(λx.xx)(λx.f(xx))
para lógica combinatória:
λf.(λx.xx)(λx.f(xx)) = S(K(λx.xx))(λf.λx.f(xx))
λx.f(xx) = S(Kf)(S(SKK)(SKK))
λf.λx.f(xx) = λf.S(Kf)(S(SKK)(SKK))
λf.S(Sf)(S(SKK)(SKK)) = S(λf.S(Sf))(K(S(SKK)(SKK)))
λf.S(Sf) = S(KS)S
λf.λx.f(xx) = S(S(KS)S)(K(S(SKK)(SKK)))
λx.xx = S(SKK)(SKK)
λf.(λx.xx)(λx.f(xx)) = S(K(S(SKK)(SKK)))(S(S(KS)S)(K(S(SKK)(SKK))))
Como estamos usando a notação de prefixo, é isso ```S`K``S``SKK``SKK``S``S`KSS`K``SKK`
.
1 Fonte: Wikipedia
O desafio
Até agora, você provavelmente já adivinhou o que é: Escreva um programa que utilize uma expressão λ válida (na notação descrita acima) como entrada e produza (ou retorne) a mesma função, reescrita no cálculo do combinador SK. Observe que há um número infinito de maneiras de reescrever isso; você só precisa produzir uma das infinitas maneiras.
Isso é código-golfe , portanto, o menor envio válido (medido em bytes) vence.
Casos de teste
Cada caso de teste mostra uma saída possível. A expressão no topo é a expressão equivalente do cálculo λ.
λx.x:
\[0] -> ``SKK
λx.xx:
\`[0][0] -> ```SKK``SKK
λx.λy.y:
\\[0] -> `SK
λx.λy.x:
\\[1] -> K
λx.λy.λz.xz(yz):
\\\``[2][0]`[1][0] -> S
λw.w(λx.λy.λz.xz(yz))(λx.λy.x):
\``[0]\\[1]\\\``[2][0]`[1][0] -> ``S``SI`KS`KK
λx.f(xx) = S(Kf)(SKK)
? Não deveria ser λx.f(xx) = S(Kf)(SII) = S(Kf)(S(SKK)(SKK))
? Ao converter λx.f(xx)
, recebo o S {λx.f} {λx.xx}
que se reduz S (Kf) {λx.xx}
e a expressão entre colchetes não é mais do que ω=λx.xx
, que sabemos que é representada como SII = S(SKK)(SKK)
, certo?
SII
, não SKK
. Isso foi um erro.