I’ve been reading a book about Common Lisp lately (Land of Lisp) and it’s given me a new appreciation for object-oriented programming
specifically what I love about OOP is how it adds context to data and data manipulations. so that your code doesn’t just show what specific data manipulations you’re doing, but also why you’re doing those manipulations, what implications those manipulations have for the meaning of the data, and even what exact format of data is expected in the first place
for example here’s a data structure that represents all of the paths that you can take in a text adventure game:
paths = [
["living-room", ["garden", "west", "door"],
["attic", "upstairs", "ladder"]],
["garden", ["living-room", "east", "door"]],
["attic", ["living-room", "downstairs", "ladder"]],
]
can you tell what each section of this data is meant to represent? it’s not exactly self-explanatory
but how about now?:
living_room_paths = AdventureGamePaths(
location="living-room",
paths=[
AdventureGamePath(
leads_to="garden",
direction="west",
description="door",
),
AdventureGamePath(
leads_to="attic",
direction="upstairs",
description="ladder",
),
],
)
garden_paths = AdventureGamePaths(
location="garden",
paths=[
AdventureGamePath(
leads_to="living-room",
direction="east",
description="door",
),
],
)
attic_paths = AdventureGamePaths(
location="attic",
paths=[
AdventureGamePath(
leads_to="living-room",
direction="downstairs",
description="ladder",
),
],
)
paths: dict[str, AdventureGamePaths] = {
"living-room": living_room_paths,
"garden": garden_paths,
"attic": attic_paths,
}
obviously this code is dramatically more verbose but it’s also so much clearer what all of those strings actually mean. grouping data into objects adds so much context and meaning to the data. it also ensures that I’m following exactly the right data format at every step
now let’s pretend that we’re working with the first style of data structure (nesting simple structures like lists and dicts) and we want to make a function that prints descriptions of all of the paths from a given room:
def describe_path(path):
return "there is a " + path[2] + " going " + path[1] + " of here"
def describe_paths(location, paths):
return '\n'.join(map(describe_path, paths[location]))
looking at this, can you tell exactly what format of data structure describe_paths() accepts for its path argument? or what about the data structure that describe_path expects? you can definitely figure it out eventually but it’ll take some sleuthing and some assumptions on the part of the reader
also, where is the data that these functions are meant to operate on? is it in this file, or somewhere else? it’s disconnected from the functions, so it could be anywhere. so if you have a nasty monolithic data structure in an unknown format, how are you supposed to figure out which functions you can use on it and which functions you can’t? how do you pull up a list of useful operations for that specific data?
you might be thinking that I’m deliberately making my code overly terse and arcane, but this is a 1:1 recreation of some example code in my Lisp book - I just converted it into Python instead
now compare those two functions above to this instead:
@dataclass
class AdventureGamePath:
leads_to: str
direction: str
description: str
def describe(self) -> str:
return "there is a " + path.description + " going " + path.direction + " of here"
@dataclass
class AdventureGamePaths:
location: str
paths: list[AdventureGamePath]
def describe_paths(self, location: str) -> str:
path_descriptions = ""
for path in self.paths:
path_descriptions += path.describe() + "\n"
return path_descriptions
again, this is much more verbose, but it’s also much clearer isn’t it? now if I see an AdventureGamePaths object I know exactly what it represents (more than one AdventureGamePath) and exactly what I can do with it (I can tell it to describe its paths)
I feel like functional programming tends to result in code that’s very terse but that doesn’t have much context behind it. there are often weird data structures left lying around without any hint about what they represent or what you can do with them - and lying next to them are arcane one-liner functions that may or may not be meant to operate on those data structures. maybe if you stare at those one-liner functions for long enough you can figure out what data manipulation they do, but what does that data manipulation mean?
so I have a renewed appreciation for classes and objects because they allow you to:
with all of this said, I don’t think that all functional programming is doomed to be arcane and unclear. I’ve heard about a concept called “typeclasses” that some functional programming languages have. I don’t know too much about them but it sounds like they’re a mathy functional programming take on classes. and those might be able to replicate the advantages of classes and objects without exactly being classes and objects
@kasdeya It is I, a local haskeller! And I've come to discuss.
Before I delve into the topic of type classes, I'd like to note something that Haskell can do to make arguments to data less arcane.
Suppose we have, indeed, these paths in question. We could create a custom type with the data keyword like so
data AdventureGamePaths = AdventureGamePaths String [(String, String, String)]
Well that wasn't awfully helpful was it? Perhaps we'd like to simplify a single path too.
data AdventureGamePath = AdventureGamePath String String String
Right, it's still magic strings with no descriptor to them. This sucks. This is where we dig out the type for making synonyms.
type LeadsTo = String
type Direction = String
type Description = String
data AdventureGamePath = AdventureGamePath LeadsTo Direction String
Ah, much easier to read! And we could also do this for location on the higher level.
Continues in next post…
@kasdeya So going to type that whole post number one again here, let's hope my narrative stays coherent.
So Haskell actually does have something to address these troubles. Sure enough we could go with raw
[(String, [String], [String])]
This is guaranteed to be arcane as soon as one takes three steps back. But we can approach the definitions provided by more object oriented languages with tools in base Haskell. Let's begin with the data keyword that lets us make custom types.
data AdventureGamePath = AdventureGamePath String String String
data AdventureGamePaths = AdventureGamePaths String [AdventureGamePath]
A little better. But we can make it more approachable with type synonyms through the type keyword.
type LeadsTo = String
type Direction = String
type Description = String
data AdventureGamePath = AdventureGamePath LeadsTo Direction Description
Continued in https://tech.lgbt/@fargate/115567172602080458
@kasdeya the deal with type classes is that they are akin to Java's Interfaces or Superclasses (I'm afraid I never learnt enough Python to draw the same parallels with ease), meaning that they describe what kind of functions any type that's a member of the type class must have for working with it. For our use case, we could make
class SelfDescribing a where
describeSelf :: a -> String
Now we have something that we can guarantee something to be self describing as a type, if it's added under this one. And we'll do it like so:
instance SelfDescribing AdventureGamePath where
describeSelf adventureGamePath = describeAdventurePath adventureGamePath
instance SelfDescribing AdventureGamePaths where
describeSelf adventureGamePaths = describeAdventurePaths adventureGamePaths
Now both could use describeSelf. Poor naming in the grand scheme of things I think, but it illustrates the example.
Slightly more rambling in next post…
@kasdeya Now, we do note that directly calling describeAdventurePath in describeAdventurePaths is no longer good hygiene, so we could reimplement it with describeSelf like so:
describeAdventurePaths :: AdventureGamePaths -> String
describeAdventureGamePaths (AdventureGamePaths location paths) = map (\path -> describeSelf path ++ "\n") paths
So yeah, functional programming does come with the tools to make this more verbose and readable. Did I catch the main gist of the complaint, or did my attempt at showcasing the features miss the mark?
@fargate nope you definitely got it! this was a very interesting read and it definitely addressed my main complaint, about functional programming seemingly have no way to annotate what kind of data a function takes, or how it transforms that data, or what that data means. it looks like in Haskell you can do all three! thanks for explaining all of this
and I’m really glad that Haskell has features like this. I’ve been interested in learning it for a while to be honest. it and also F#
I’m really hoping that Common Lisp will turn out to have this kind of thing too, because at the moment I’ve just been leaving comments where I give everything Python-style type annotations lol just so that I can keep track of all of the datatypes in my head