Il calcolo λ , o calcolo lambda, è un sistema logico basato su funzioni anonime. Ad esempio, questa è un'espressione λ:
λf.(λx.xx)(λx.f(xx))
Tuttavia, ai fini di questa sfida, semplificheremo la notazione:
- Passare
λ
a\
(per semplificare la digitazione):\f.(\x.xx)(\x.f(xx))
- Le
.
intestazioni in lambda non sono necessarie, quindi possiamo rilasciarle:\f(\xxx)(\xf(xx))
- Utilizzare la notazione con prefisso in stile Unlambda con
`
per l'applicazione anziché scrivere insieme le due funzioni (per una spiegazione completa di come eseguire questa operazione, vedere Converti tra le notazioni di calcolo Lambda ):\f`\x`xx\x`f`xx
- Questa è la sostituzione più complicata. Sostituisci ogni variabile con un numero tra parentesi in base alla profondità con cui la variabile è nidificata rispetto all'intestazione lambda a cui appartiene (ovvero usa l' indicizzazione De Bruijn basata su 0 ). Ad esempio, in
\xx
(la funzione di identità), l'x
elemento nel corpo verrebbe sostituito con[0]
, poiché appartiene alla prima intestazione (basata su 0) riscontrata quando si attraversa l'espressione dalla variabile alla fine;\x\y``\x`xxxy
sarebbe convertito in\x\y``\x`[0][0][1][0]
. Ora possiamo rilasciare le variabili nelle intestazioni, lasciando\\``\`[0][0][1][0]
.
La logica combinatoria è fondamentalmente un Tarpit di Turing fatto con il calcolo λ (Beh, in realtà, è venuto per primo, ma qui non ha importanza.)
"La logica combinatoria può essere vista come una variante del calcolo lambda, in cui le espressioni lambda (che rappresentano l'astrazione funzionale) sono sostituite da un insieme limitato di combinatori, funzioni primitive da cui sono assenti variabili associate." 1
Il tipo più comune di logica combinatoria è il calcolo del combinatore SK , che utilizza le seguenti primitive:
K = λx.λy.x
S = λx.λy.λz.xz(yz)
A volte I = λx.x
viene aggiunto un combinatore , ma è ridondante, poiché SKK
(o effettivamente SKx
per qualsiasi x
) è equivalente a I
.
Tutto ciò di cui hai bisogno è K, S e l'applicazione per poter codificare qualsiasi espressione nel calcolo λ. Ad esempio, ecco una traduzione dalla funzione λf.(λx.xx)(λx.f(xx))
alla logica 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))))
Dal momento che stiamo usando la notazione del prefisso, questo è ```S`K``S``SKK``SKK``S``S`KSS`K``SKK`
.
1 Fonte: Wikipedia
La sfida
Ormai probabilmente hai indovinato cosa sia: scrivi un programma che accetta un'espressione λ valida (nella notazione sopra descritta) come input e output (o restituisce) la stessa funzione, riscritta nel calcolo del combinatore SK. Nota che ci sono un numero infinito di modi per riscriverlo; devi solo produrre uno dei modi infiniti.
Si tratta di code-golf , quindi vince l'invio valido più breve (misurato in byte).
Casi test
Ogni caso di test mostra un possibile output. L'espressione in alto è l'espressione equivalente del calcolo λ.
λ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)
? Non dovrebbe piuttosto essere λx.f(xx) = S(Kf)(SII) = S(Kf)(S(SKK)(SKK))
? Durante la conversione λx.f(xx)
, ottengo ciò S {λx.f} {λx.xx}
che si riduce a S (Kf) {λx.xx}
e l'espressione tra parentesi non è altro che ω=λx.xx
, che sappiamo essere rappresentato come SII = S(SKK)(SKK)
, giusto?
SII
, no SKK
. È stato un errore.