Boolsk logik med SKI
Kombinatorer er ikke ligefrem nemme at forstÄ. Som programmÞr har jeg mest gavn af at skrive kode, der viser det begreb, jeg prÞver at fÄ greb om. Jeg lÊrer bedst ved selv at arbejde med det.
I dette indlĂŠg implementerer jeg de tre grundlĂŠggende kombinatorer i SKI-kombinatorlogikken. Derefter tilfĂžjer jeg boolske operatorer og afprĂžver De Morgans love.
Jeg er ikke ekspert. Det er blot noter fra mit eget forsÞg pÄ at forstÄ emnet, og jeg hÄber, at de kan hjÊlpe andre, der prÞver at forstÄ det samme.
Kombinatorfugle
I sin berĂžmte bog "To Mock a Mockingbird and Other Logic Puzzles: Including an Amazing Adventure in Combinatory Logic" bruger Raymond Smullyan fugle som metafor for kombinatorer:
I en fortryllet skov bor der talende fugle. Hvis du har to vilkÄrlige fugle A og B og rÄber B's navn til A, svarer A ved at rÄbe navnet pÄ en anden fugl til dig. Denne fugl kalder vi AB. AB er altsÄ den fugl, som A nÊvner efter at have hÞrt B's navn. I stedet for hele tiden at bruge den besvÊrlige formulering "A's svar pÄ at hÞre B's navn" siger vi mere enkelt: "A's svar pÄ B". AB er dermed A's svar pÄ B. A's svar pÄ B er ikke nÞdvendigvis det samme som B's svar pÄ A. Med symboler betyder det, at AB ikke nÞdvendigvis er den samme fugl som BA. Hvis vi har tre fugle A, B og C, er fuglen A(BC) heller ikke nÞdvendigvis den samme som fuglen (AB)C. Fuglen A(BC) er A's svar pÄ fuglen BC, mens fuglen (AB)C er fuglen AB's svar pÄ fuglen C. Parenteserne er derfor nÞdvendige for at undgÄ tvetydighed. Hvis jeg blot skrev ABC, kunne du ikke vide, om jeg mente fuglen A(BC) eller fuglen (AB)C.
Jeg gennemgÄr ikke bogen kapitel for kapitel her, men jeg hÄber at vende tilbage til dele af den i senere indlÊg.
Nogle fugle tager andre fugle som input og returnerer fugle som output. Det er en fin analogi til funktioner. Kombinatorerne er funktioner, og vi giver dem fuglenavne.
Lad os begynde med den mÄske enkleste kombinator:
Idiot λa.a
Nogle kalder den Identity-fuglen. Selv om den er enkel, fÄr vi senere brug for den. SÄdan definerer vi en Idiot i TypeScript:
export type Idiot = <T>(a: T) => T
export const I: Idiot = a => a
Den tager en vĂŠrdi og returnerer den uĂŠndret:
describe('Idiot', () => {
const x = 'whatever';
test('Ix = x', () => {
expect(I(x)).toBe(x);
});
);
Kestrel λab.a
Kestrel kaldes ogsÄ constant eller TRUE. Den tager en vÊrdi og returnerer den igen, uanset hvad man sender ind bagefter:
export type Kestrel = <T0, T1>(a: T0) => (b: T1) => T0
export const K: Kestrel = a => b => a
describe("Kestrel", () => {
test("Kxy = x", () => {
const x = "x"
const y = "y"
const actual = K(x)(y)
expect(actual).toBe(x)
})
})
Starling λabc.ac(bc)
Starling, ogsÄ kaldet substitutionsoperatoren, er mere kompliceret. Den tager tre argumenter og returnerer a(c)(b(c)). FÞrst anvender den a pÄ c og b pÄ c. Derefter anvender den resultatet af a(c) pÄ resultatet af b(c):
export type Starling = <TC>(
a: (c: TC) => any
) => (b: (c: TC) => any) => (c: TC) => any
export const S: Starling = a => b => c => a(c)(b(c))
describe("Starling", () => {
test("Sxyz = xz(yz)", () => {
const x = _ => I
const y = a => a() * 2
const z = () => 3
const xz = x(z)
const yz = y(z)
const actual = S(x)(y)(z)
expect(actual).toBe(xz(yz))
expect(actual).toBe(6)
})
})
Ix = SSKKx = SK(KK)x = x
Vi kan bygge enhver kombinator, inklusive I, udelukkende med K og S:
describe("Idiot", () => {
test("Ix = SSKKx = x", () => {
expect(S(S)(K)(K)(x)).toBe(I(x))
})
test("Ix = SK(KK)x = x", () => {
const sk = S(K)
const kk = K(K)
expect(sk(kk)(x)).toBe(I(x))
})
})
TRUE = K
Antag, at vi har et ordnet par med to vĂŠrdier: true og false. Da Kestrel altid returnerer den fĂžrste vĂŠrdi, kan vi kalde den TRUE:
export const TRUE = K
const t = () => true
const f = () => false
describe("TRUE = K", () => {
test("Ktf = (TRUE)tf = t", () => {
const actual = TRUE(t)(f)()
expect(actual).toBe(K(t)(f)())
expect(actual).toBe(true)
})
})
FALSE = SK = KI
PÄ samme mÄde kan vi bygge en kombinator, der altid returnerer FALSE:
export const FALSE = S(K)
describe("FALSE = SK", () => {
test("SKxy = y", () => {
expect(FALSE(t)(f)()).toBe(false)
})
test("KItf = (FALSE)tf = f", () => {
const actual = K(I)(t)(f)()
expect(actual).toBe(FALSE(t)(f)())
expect(actual).toBe(false)
})
})
BemĂŠrk, at den anden test bruger KI i stedet for SK. I er ikke nĂždvendig, men kan bruges som syntaktisk sukker.
NOT = (SK)(K)
Her bliver det mere kompliceret, sÄ lad os stole pÄ testene frem for intuitionen. Her er NOT:
export const NOT = b => b(S(K))(K)
describe("NOT = (SK)(K)", () => {
test("NOT = (SK)(K)", () => {
const sk = S(K)
expect(TRUE(sk(K))(t)(f)()).toBe(false)
expect(TRUE(sk(K))(f)(t)()).toBe(true)
})
test("NOT(TRUE) = FALSE", () => {
expect(NOT(TRUE)(t)(f)()).toBe(false)
expect(NOT(TRUE)(f)(t)()).toBe(true)
})
test("NOT(FALSE) = TRUE", () => {
expect(NOT(FALSE)(t)(f)()).toBe(true)
expect(NOT(FALSE)(f)(t)()).toBe(false)
})
test("TRUE(FALSE)(TRUE) = FALSE", () => {
expect(TRUE(FALSE)(TRUE)(t)(f)()).toBe(false)
})
test("FALSE(FALSE)(TRUE) = TRUE", () => {
expect(FALSE(FALSE)(TRUE)(t)(f)()).toBe(true)
})
})
Denne definition af NOT adskiller sig lidt fra den, der stÄr pÄ Wikipedia. Svaret pÄ mit spÞrgsmÄl pÄ Stack Overflow forklarer hvorfor.
OR = TRUE
export const OR = TRUE
describe("OR = T = K", () => {
test("(T)OR(T) = T(T)(T) = T", () => {
expect(TRUE(OR)(TRUE)(t)(f)()).toBe(true)
})
test("(T)OR(F) = T(T)(F) = T", () => {
expect(TRUE(OR)(FALSE)(t)(f)()).toBe(true)
})
test("(F)OR(T) = F(T)(T) = T", () => {
expect(FALSE(OR)(TRUE)(t)(f)()).toBe(true)
})
test("(F)OR(F) = F(T)(F) = F", () => {
expect(FALSE(OR)(FALSE)(t)(f)()).toBe(false)
})
})
AND = FALSE
export const AND = FALSE
describe("AND = F = SK", () => {
test("(T)(T)AND = T(T)(F) = T", () => {
expect(TRUE(TRUE)(AND)(t)(f)()).toBe(true)
})
test("(T)(F)AND = T(F)(F) = F", () => {
expect(TRUE(FALSE)(AND)(t)(f)()).toBe(false)
})
test("(F)(T)AND = F(T)(F) = F", () => {
expect(FALSE(TRUE)(AND)(t)(f)()).toBe(false)
})
test("(F)(F)AND = F(F)(F) = F", () => {
expect(FALSE(FALSE)(AND)(t)(f)()).toBe(false)
})
})
De Morgans love
Med det boolske logiksystem kan vi efterprĂžve De Morgans love:
describe("De Morgan's Laws", () => {
const or = (a, b) => a(OR)(b)
const and = (a, b) => a(b)(AND)
const not = a => NOT(a)
test("ÂŹ(a âš b) â (ÂŹa) â§ (ÂŹb)", () => {
expect(!(true || true)).toBe(false)
expect(!true && !true).toBe(false)
expect(not(or(TRUE, TRUE))(t)(f)()).toBe(false)
expect(and(not(TRUE), not(TRUE))(t)(f)()).toBe(false)
expect(!(true || false)).toBe(false)
expect(!true && !false).toBe(false)
expect(not(or(TRUE, FALSE))(t)(f)()).toBe(false)
expect(and(not(TRUE), not(FALSE))(t)(f)()).toBe(false)
expect(!(false || false)).toBe(true)
expect(!false && !false).toBe(true)
expect(not(or(FALSE, FALSE))(t)(f)()).toBe(true)
expect(and(not(FALSE), not(FALSE))(t)(f)()).toBe(true)
expect(!(false || true)).toBe(false)
expect(!false && !true).toBe(false)
expect(not(or(FALSE, TRUE))(t)(f)()).toBe(false)
expect(and(not(FALSE), not(TRUE))(t)(f)()).toBe(false)
})
test("ÂŹ(a â§ b) â (ÂŹa) âš (ÂŹb)", () => {
expect(!(true && true)).toBe(false)
expect(!true || !true).toBe(false)
expect(not(and(TRUE, TRUE))(t)(f)()).toBe(false)
expect(or(not(TRUE), not(TRUE))(t)(f)()).toBe(false)
expect(!(true && false)).toBe(true)
expect(!true || !false).toBe(true)
expect(not(and(TRUE, FALSE))(t)(f)()).toBe(true)
expect(or(not(TRUE), not(FALSE))(t)(f)()).toBe(true)
expect(!(false && false)).toBe(true)
expect(!false || !false).toBe(true)
expect(not(and(FALSE, FALSE))(t)(f)()).toBe(true)
expect(or(not(FALSE), not(FALSE))(t)(f)()).toBe(true)
expect(!(false && false)).toBe(true)
expect(!false || !false).toBe(true)
expect(not(and(FALSE, FALSE))(t)(f)()).toBe(true)
expect(or(not(FALSE), not(FALSE))(t)(f)()).toBe(true)
expect(!(false && true)).toBe(true)
expect(!false || !true).toBe(true)
expect(not(and(FALSE, TRUE))(t)(f)()).toBe(true)
expect(or(not(FALSE), not(TRUE))(t)(f)()).toBe(true)
})
})