Converter expressões λ em expressões SK


20

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), o xno 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`xxxyseria 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 SKxpara 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 é , 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


1
Eu acho que seu segundo caso de teste não está correto. O último contém um número que não está entre colchetes.
Christian Sievers


Como você conseguiu λ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?
BarbaraKwarc

@BarbaraKwarc Certo, eu quis dizer SII, não SKK. Isso foi um erro.
Esolanging Fruit

Respostas:


9

Haskell, 251 237 222 214 bytes

15 bytes salvos graças a @ Ørjan_Johansen (veja também seus links TIO nos comentários)!

Mais 8 bytes salvos graças ao @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

panalisa a entrada, retornando a parte não analisada restante no primeiro componente do par resultante. O primeiro caractere de seu argumento deve ser um backtick, uma barra invertida ou um colchete de abertura. Os guardas de padrão pverificam esses casos nesta ordem. No primeiro caso, denotando um aplicativo, mais duas expressões são analisadas e combinadas a um elemento do Etipo de dados com o construtor infix :>. No caso lambda, a seguinte expressão é analisada e fornecida imediatamente à afunção. Caso contrário, é uma variável, obtemos seu número com a readsfunção (que retorna uma lista) e eliminamos o colchete de fechamento pelo padrão correspondente a(_:t) .

A afunção faz a abstração de colchete bastante conhecida. Para abstrair uma aplicação, abstraímos as duas subexpressões e usamos o Scombinador para distribuir o argumento para ambas. Isso está sempre correto, mas com mais código, podemos fazer muito melhor tratando casos especiais para obter expressões mais curtas. Abstrair a variável atual fornece Iou, quando não temos isso SKK,. Normalmente, os casos restantes podem apenas adicionar um Kà frente, mas ao usar essa notação, temos que renumerar as variáveis ​​conforme o lambda interno é abstraído.

otransforma o resultado em uma string para saída. fé a função completa.

Como em muitos idiomas, a barra invertida é um caractere de escape, portanto deve ser fornecida duas vezes em uma string literal:

*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. Na segunda linha, você pode usar (a,(b,v))<-p<$>p s. 2. '\\'Pode ser apenas _se você mover a partida por último.
Ørjan Johansen

1
Na verdade, raspe a primeira parte: é mais curto trocar a ordem da tupla e usá p(_:s)=a<$>p s-la na '\\'linha (movida) .
Ørjan Johansen

1
Experimente online! para sua versão atual. Que, a propósito, tem apenas 236 bytes, você pode soltar a nova linha final.
Ørjan Johansen

2
@ Challenger5 Eu acho que é principalmente devido ao fato de que Haskell é baseado em cálculo lambda, para que as pessoas proficientes em Haskell são mais propensos a ser atraídos para este tipo de perguntas :)
Leo

2
Você pode definir pcom uma única expressão com três guardas, reorganizar os casos e soltar um par supérfluo de (): 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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.