KB-Exper
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.
Top comments (0)