KB-Exper
from veer66
KB-Exper is a collection of knowledge representations based on diverse approaches. If knowledge representation sounds unfamiliar, in this context it simply refers to how structured data is stored.
Approaches
Dispatch tables (Python dictionaries)
The dispatch-table approach scattered data into many dictionaries acting as key-value maps, for example:
os_to_inception.py:
OS_TO_INCEPTION = {
OS.CPM: 1974,
OS.NETBSD: 1993,
OS.PCDOS: 1981
}
os_to_platforms.py:
OS_TO_PLATFORMS = {
OS.CPM: frozenset({Platform.I8080, Platform.I8085, Platform.I8086, Platform.M68K, Platform.Z80, Platform.Z8K}),
OS.NETBSD: frozenset({Platform.ALPHA, Platform.ARM, Platform.M68K, Platform.MIPS, Platform.PARISC, Platform.SUPERH, Platform.VAX, Platform.X86, Platform.RISCV}),
OS.PCDOS: frozenset({Platform.X86}),
}
os_to_proglangs.py:
OS_TO_PROGLANGS = {
OS.CPM: frozenset({ProgLang.ASM, ProgLang.PLM}),
OS.NETBSD: frozenset({ProgLang.ASM, ProgLang.C}),
OS.PCDOS: frozenset({ProgLang.ASM, ProgLang.C}),
}
main.py:
def os_to_str(os: OS) -> str:
inception = OS_TO_INCEPTION[os]
return f"{os.value}@{inception}"
if __name__ == '__main__':
print(list(map(os_to_str,OS)))
The main advantage is that the data is decoupled. We can add another
key-value map without editing existing code (the Open—Closed
Principle). os_to_str depends only on the map it needs, not on the
rest.
The disadvantage is that the data is scattered (low cohesion). For example, adding a new OpenBSD entry requires editing four files.
Python dataclass
We can put all attributes into a dataclass.
kb.py:
OSES = [
OS(name="CP/M",
inception=1974,
platforms=frozenset({Platform.I8080, Platform.I8085, Platform.I8086, Platform.M68K, Platform.Z80, Platform.Z8K}),
proglangs=frozenset({ProgLang.ASM, ProgLang.PLM}),
),
OS(name="NetBSD",
inception=1993,
platforms=frozenset({Platform.ALPHA, Platform.ARM, Platform.M68K, Platform.MIPS, Platform.PARISC, Platform.SUPERH, Platform.VAX, Platform.X86, Platform.RISCV}),
proglangs=frozenset({ProgLang.ASM, ProgLang.C}),
),
OS(name="PC-DOS",
inception=1981,
platforms=frozenset({Platform.X86}),
proglangs=frozenset({ProgLang.ASM, ProgLang.C})
),
]
main.py:
class NameInception(Protocol):
@property
def name(self) -> str: ...
@property
def inception(self) -> int: ...
def name_inception_to_str(name_inception: NameInception) -> str:
return f"{name_inception.name}@{name_inception.inception}"
if __name__ == '__main__':
print(list(map(name_inception_to_str, OSES)))
This approach has high coherence; for example, adding OpenBSD could be done by editing only one block in one file. However, adding a new attribute requires editing an existing dataclass. To make a function less dependent on the dataclass, a protocol can be used, but it produces more boilerplate.
Clojure map
Using maps to store data and using keywords as keys is idiomatic in Clojure. The code is more concise. However, this approach does not support static type checking. A runtime validator can be used instead, but it is not included in this example.
kb part:
(def oses [{:name "CP/M"
:inception 1974
:platforms #{:I8080 :I8085 :I8086 :M68K :Z80 :Z8K}
:proglangs=#{:ASM, :PLM}}
{:name "NetBSD"
:inception 1993
:platforms #{:ALPHA, :ARM, :M68K, :MIPS, :PARISC, :SUPERH, :VAX, :X86, :RISCV}
:proglangs #{:ASM, :C}}
{:name "PC-DOS"
:inception 1981,
:platforms #{:X86}
:proglangs #{:ASM, :C}}])
main part:
(defn name-inception-to-str [{:keys [name inception]}]
(str name "@" inception))
(println (map name-inception-to-str oses))
Storing all attributes in a Clojure map results in high cohesion, and with dynamic typing, a function does not depend on a type. The code is more concise than the Python dataclass approach. However, adding a new attribute still requires editing the existing file, and there is no static type checking (Typed Clojure can be used instead if static type checking is needed).
Logic (Prolog)
We can store our OS information as Prolog facts. In this example, the facts are grouped by OS, which leans toward the dataclass approach; however, we can also group facts by their predicates (e.g., platform, proglang), which would lean more toward the dispatch table approach.
% CP/M
name(cpm,"CP/M").
inception(cpm,1974).
platform(cpm,i8080).
platform(cpm,i8085).
platform(cpm,i8086).
platform(cpm,m68k).
platform(cpm,z80).
platform(cpm,z8k).
proglang(cpm,asm).
proglang(cpm,plm).
% NetBSD
name(netbsd,"NetBSD").
inception(netbsd,1993).
platform(netbsd,alpha).
platform(netbsd,arm).
platform(netbsd,m68k).
platform(netbsd,mips).
platform(netbsd,parisc).
platform(netbsd,superh).
platform(netbsd,vax).
platform(netbsd,x86).
platform(netbsd,riscv).
% PC-DOS
name(pcdos,"PC-DOS").
inception(pcdos,1981).
platform(pcdos,x86).
proglang(pcdos,asm).
proglang(pcdos,c).
main :- forall((name(OS, Name), inception(OS, Year)),
format("~w@~w~n", [Name, Year])).
In the Prolog example, we use a rule to produce strings of name and inception instead of a function. The rule is concise and does not depend on unnecessary facts.
The advantage of Prolog is that we can switch between higher cohesion and lower coupling just by changing the way we group the facts, while every fact stays the same.
Since few people are familiar with Prolog, Datalog adapted to Python or Clojure can be used instead, but it would be harder to get accepted than a more idiomatic approach.