El cálculo λ , o cálculo lambda, es un sistema lógico basado en funciones anónimas. Por ejemplo, esta es una expresión λ:
λf.(λx.xx)(λx.f(xx))
Sin embargo, para los propósitos de este desafío, simplificaremos la notación:
- Cambie
λ
a\
(para que sea más fácil escribir):\f.(\x.xx)(\x.f(xx))
- Los
.
encabezados in lambda son innecesarios, por lo que podemos descartarlos:\f(\xxx)(\xf(xx))
- Utilice la notación de prefijo de estilo Unlambda con
`
para la aplicación en lugar de escribir las dos funciones juntas (para obtener una explicación completa de cómo hacerlo, consulte Convertir entre anotaciones de cálculo Lambda ):\f`\x`xx\x`f`xx
- Esta es la sustitución más complicada. Reemplace cada variable con un número entre paréntesis según cuán profundamente anidada esté la variable en relación con el encabezado lambda al que pertenece (es decir, use la indexación de De Bruijn basada en 0 ). Por ejemplo, en
\xx
(la función de identidad), elx
en el cuerpo sería reemplazado por[0]
, porque pertenece al primer encabezado (basado en 0) encontrado al atravesar la expresión desde la variable hasta el final;\x\y``\x`xxxy
sería convertido en\x\y``\x`[0][0][1][0]
. Ahora podemos soltar las variables en los encabezados, dejando\\``\`[0][0][1][0]
.
La lógica combinatoria es básicamente un Tarpit de Turing hecho del cálculo λ (Bueno, en realidad, llegó primero, pero eso es irrelevante aquí).
"La lógica combinatoria puede verse como una variante del cálculo lambda, en el que las expresiones lambda (que representan la abstracción funcional) se reemplazan por un conjunto limitado de combinadores, funciones primitivas de las que las variables ligadas están ausentes". 1
El tipo más común de lógica combinatoria es el cálculo del combinador SK , que utiliza las siguientes primitivas:
K = λx.λy.x
S = λx.λy.λz.xz(yz)
A veces I = λx.x
se agrega un combinador , pero es redundante, ya que SKK
(o de hecho SKx
para cualquiera x
) es equivalente a I
.
Todo lo que necesita es K, S y una aplicación para poder codificar cualquier expresión en el cálculo λ. Como ejemplo, aquí hay una traducción de la función λf.(λx.xx)(λx.f(xx))
a la lógica combinatoria:
λ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 la notación de prefijo, esto es ```S`K``S``SKK``SKK``S``S`KSS`K``SKK`
.
1 fuente: Wikipedia
El reto
A estas alturas, probablemente haya adivinado qué es: escribir un programa que tome una expresión λ válida (en la notación descrita anteriormente) como entrada y salida (o devuelve) la misma función, reescrita en el cálculo del combinador SK. Tenga en cuenta que hay infinitas formas de reescribir esto; solo necesita generar una de las infinitas formas.
Este es el código de golf , por lo que gana el envío válido más corto (medido en bytes).
Casos de prueba
Cada caso de prueba muestra una salida posible. La expresión en la parte superior es la expresión equivalente de 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)
? ¿No debería ser más bien λx.f(xx) = S(Kf)(SII) = S(Kf)(S(SKK)(SKK))
? Al convertir λx.f(xx)
, obtengo lo S {λx.f} {λx.xx}
que se reduce S (Kf) {λx.xx}
y la expresión entre paréntesis no es más que ω=λx.xx
, lo que sabemos está representado como SII = S(SKK)(SKK)
, ¿verdad?
SII
, no SKK
. Eso fue un error.