Convierta expresiones λ en expresiones SK


20

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), el xen 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`xxxyserí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.xse agrega un combinador , pero es redundante, ya que SKK(o de hecho SKxpara 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 , 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


1
Creo que su segundo caso de prueba no es correcto. El último contiene un número que no está entre paréntesis.
Christian Sievers


¿Cómo se consigue λ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?
BarbaraKwarc

@BarbaraKwarc Correcto, quise decir SII, no SKK. Eso fue un error.
Esolanging Fruit

Respuestas:


9

Haskell, 251 237 222 214 bytes

¡15 bytes guardados gracias a @ Ørjan_Johansen (también vea sus enlaces TIO en los comentarios)!

¡8 bytes más guardados gracias a @nimi!

data E=S|K|E:>E|V Int
p(h:s)|h>'_',(u,a)<-p s,(v,b)<-p u=(v,a:>b)|h>'['=a<$>p s|[(n,_:t)]<-reads s=(t,V n)
a(e:>f)=S:>a e:>a f
a(V 0)=S:>K:>K
a(V n)=K:>V(n-1)
a x=K:>x
o(e:>f)='`':o e++o f
o S="S"
o K="K"
f=o.snd.p

panaliza la entrada y devuelve la parte no analizada restante en el primer componente del par resultante. El primer carácter de su argumento debe ser una barra de retroceso, una barra diagonal inversa o un paréntesis de apertura. Los guardias del patrón pverifican estos casos en este orden. En el primer caso, que denota una aplicación, se analizan dos expresiones más y se combinan en un elemento del Etipo de datos con el constructor infijo :>. En el caso lambda, la siguiente expresión se analiza y se le da inmediatamente a la afunción. De lo contrario, es una variable, obtenemos su número con la readsfunción (que devuelve una lista) y soltamos el corchete de cierre haciendo coincidir el patrón con(_:t) .

La afunción realiza la abstracción de paréntesis bastante conocida. Para abstraer una aplicación, abstraemos las dos subexpresiones y usamos el Scombinador para distribuir el argumento a ambas. Esto siempre es correcto, pero con más código podríamos hacerlo mucho mejor manejando casos especiales para obtener expresiones más cortas. Al abstraer la variable actual se obtiene Io, cuando no tenemos eso SKK,. Por lo general, los casos restantes solo pueden agregar unK frente al frente, pero al usar esta notación tenemos que renumerar las variables a medida que se abstrae el lambda interno.

o convierte el resultado en una cadena para la salida. fEs la función completa.

Como en muchos idiomas, la barra diagonal inversa es un carácter de escape, por lo que debe darse dos veces en un literal de cadena:

*Main> f "\\[0]"
"``SKK"
*Main> f "\\`[0][0]"
"``S``SKK``SKK"
*Main> f "\\\\[0]"
"``S``S`KS`KK`KK"
*Main> f "\\\\[1]"
"``S`KK``SKK"
*Main> f "\\\\\\``[2][0]`[1][0]"
"``S``S`KS``S``S`KS``S`KK`KS``S``S`KS``S``S`KS``S`KK`KS``S``S`KS``S`KK`KK``S`KK``SKK``S``S`KS``S``S`KS``S`KK`KS``S`KK`KK``S`KK`KK``S``S`KS``S``S`KS``S`KK`KS``S``S`KS``S`KK`KK``S``S`KS`KK`KK``S``S`KS``S``S`KS``S`KK`KS``S`KK`KK``S`KK`KK"

1
1. En la segunda línea, puede usar (a,(b,v))<-p<$>p s. 2. '\\'Puede ser solo _si mueves ese partido en último lugar.
Ørjan Johansen

1
En realidad, rasca la primera parte: es más corto intercambiar el orden de las tuplas y usarlo p(_:s)=a<$>p spara la '\\'línea (movida) .
Ørjan Johansen

1
Pruébalo en línea! para su versión actual Que por cierto solo tiene 236 bytes, puede soltar la nueva línea final.
Ørjan Johansen

2
@ Challenger5 Creo que se debe principalmente al hecho de que Haskell se basa en el cálculo lambda, por lo que es más probable que las personas que dominan Haskell se sientan atraídas por este tipo de preguntas :)
Leo

2
Se puede definir pcon una sola expresión con tres guardias, reorganizar los casos y soltar un par de superflua (): p(h:s)|h>'_',(u,a)<-p s,(v,b)<-p u=(v,a:>b)|h>'['=a<$>p s|[(n,_:t)]<-reads s=(t,V n).
nimi
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.