8

For a few weeks I’ve been thinking about relation between objects – not especially OOP’s objects. For instance in C++, we’re used to representing that by layering pointers or container of pointers in the structure that needs an access to the other object. If an object A needs to have an access to B, it’s not uncommon to find a B *pB in A.

But I’m not a C++ programmer anymore, I write programs using functional languages, and more especially in Haskell, which is a pure functional language. It’s possible to use pointers, references or that kind of stuff, but I feel strange with that, like “doing it the non-Haskell way”.

Then I thought a bit deeper about all that relation stuff and came to the point:

“Why do we even represent such relation by layering?

I read some folks already thought about that (here). In my point of view, representing relations through explicit graphes is way better since it enables us to focus on the core of our type, and express relations later through combinators (a bit like SQL does).

By core I mean that when we define A, we expect to define what A is made of, not what it depends on. For instance, in a video game, if we have a type Character, it’s legit to talk about Trait, Skill or that kind of stuff, but is it if we talk about Weapon or Items? I’m not so sure anymore. Then:

data Character = {
    chSkills :: [Skill]
  , chTraits :: [Traits]
  , chName   :: String
  , chWeapon :: IORef Weapon -- or STRef, or whatever
  , chItems  :: IORef [Item] -- ditto
  }

sounds really wrong in term of design to me. I’d rather prefer something like:

data Character = {
    chSkills :: [Skill]
  , chTraits :: [Traits]
  , chName   :: String
  }

-- link our character to a Weapon using a Graph Character Weapon
-- link our character to Items using a Graph Character [Item] or that kind of stuff

Furthermore, when a day comes to add new features, we can just create new types, new graphs and link. In the first design, we’d have to break the Character type, or use some kind of work around to extend it.

What do you think about that idea? What do you think is best to deal with that kind of issues in Haskell, a pure functional language?

phaazon
  • 189
  • 3
  • 2
    Polling for opinions is not allowed here. You should rephrase the question to something along the lines of "are there any drawbacks to this approach?" instead of "what do you think of this?" or "what is the best way to...?" For whatever it's worth, it doesn't make sense to ask why things are done a certain way in C++ as opposed to the way it's done in functional languages, because C++ lacks garbage collection, anything analogous to type classes (unless you use templates, which opens a really big can of worms), and many other features that facilitate functional programming. – Doval May 28 '14 at 14:03
  • One thing worth nothing is that in your approach, it's not possible to tell *using the type system* whether a `Character` *should* be able to hold weapons. When you have a map from `Characters` to `Weapons` and a character is absent from the map, does that mean the character doesn't currently hold a weapon, or that the character can't hold weapons at all? Moreover, you'd be doing unnecessary lookups because you don't know *a priori* whether a `Character` can hold a weapon or not. – Doval May 28 '14 at 14:19
  • @Doval If characters being unable to hold weapons was a feature in your game, wouldn't it make sense to then give the character record a `canHoldWeapons :: Bool` flag, which lets you know immediately if it can hold weapons, and then if your character isn't in the graph you can say that the character isn't currently holding weapons. Make `giveCharacterWeapon :: WeaponGraph -> Character -> Weapon -> WeaponGraph` just act like `id` if that flag is `False` for the character provided, or use `Maybe` to represent failure. – bheklilr May 28 '14 at 15:20
  • I like this and frankly think what you're designing fits an applicative style very well - which I tend to find common when looking at doing OO modeling problems in Haskell. Applicatives allow you to nicely and fluently compose CRUD style behaviours, so the idea that you would model bits independently, then have some applicative implementations that let you compose those types together with operations being slipped inbetween the compositions for things like validation, persistence, reactive event firing etc. This fits very much with what you suggest there. Graphs work great with applicatives. – Jimmy Hoffa May 28 '14 at 16:09
  • @bheklilr I agree, it's not insurmountable, I was just pointing out a problem in the current design. Even better would be to use two different constructors so you can use pattern matching instead of branching. – Doval May 28 '14 at 16:25
  • @Doval I'm wary of using multiple constructors with record types. While legal, you end up with incomplete functions that can fail if called on the wrong constructor. Since pattern matching just gets turned into a case statement in core anyway, it's not like you're really saving any cycles by using pattern matching over an if-then-else. – bheklilr May 28 '14 at 16:28
  • @bheklilr What language/compiler are you working in that doesn't emit warnings for incomplete functions? It's not even about efficiency, it's more about representing only the valid cases. One can imagine that having a weapon necessarily implies having an inventory. Using two boolean flags, you could produce the invalid state where `hasWeapon` is true and `hasInventory` is false. I'd only use boolean flags for attributes that are completely independent from one another (if having a weapon is independent from having an inventory, I wouldn't want to make 4 constructors.) – Doval May 28 '14 at 16:34
  • 3
    @Doval Specifically with record style data types, if you have multiple constructors the following code is perfectly legal and does not emit warnings with `-Wall` on GHC: `data Foo = Foo { foo :: Int } | Bar { bar :: String }`. It then type checks to call `foo (Bar "")` or `bar (Foo 0)`, but both will raise exceptions. If you're using positional style data constructors then there isn't a problem because the compiler doesn't generate the accessors for you, but you don't get the convenience of the record types for large structures and it takes more boilerplate to use libraries like lens. – bheklilr May 28 '14 at 16:38
  • @Doval To solve that problem of having invalid state, you could have `_hasWeapon` as the flag in the structure, then write `hasWeapon c = _hasInventory c && _hasWeapon c`, which is the "smart" accessor to use. Obviously there are other ways to solve this, but simply writing another function that returns only valid states sounds like a good implementation to me. – bheklilr May 28 '14 at 16:52
  • @bheklilr That's...interesting. I mainly work in Standard ML, which generates accessors for records, but not datatypes. I.e. For any given record with a `foo` field there is a `#foo` getter function, but there is no such function for the `Foo` datatype. In order to use `#foo` I'd first have to do case analysis to extract the record out of the `Foo` or `Bar` constructors, and attempting to use it on the record held by `Bar` would fail type checking. I didn't know this was an issue in Haskell. I take it Haskell generates incomplete functions for every field in every record in every constructor? – Doval May 28 '14 at 16:53
  • @Doval Correct, Haskell does not perform type checking on constructors, since it's impossible to know them at runtime with its data model. This could be considered an issue, but rarely do I see an ADT of records with different fields. It's just not considered good style in Haskell. – bheklilr May 28 '14 at 16:57
  • Interesting comments! I’m not really looking for “answers”, I just want to know what’s around. This is a typical problem I often experience in **Haskell** since I’m writing a 3D engine – which implies a lot of “entities” semantics. – phaazon Jun 02 '14 at 12:42

1 Answers1

1

You have actually answered your own question you just don't know it yet. The question you're asking is not about Haskell but about programming in general. You're actually asking yourself very good questions (kudos).

My approach to the problem you have at hand divides basically in two main aspects: the domain model design and trade-offs.

Let me explain.

Domain Model Design: This is how you decide to organize the core model of your application. I can see you're already doing that by thinking of Characters, Weapons and so on. Plus you need to define the relations between them. Your approach of defining objects by what they are instead of what they depend on is totally valid. Be careful though because it's not a silver bullet for every decision regarding your design.

You're definitely doing the right thing by thinking of this in advance but at some point you need to stop thinking and start writing some code. You'll see then if your early decisions were the right ones or not. Most likely not since you don't have yet full knowledge of your application. Then don't be afraid of refactoring when you realize certain decisions are becoming a problem not a solution. It's important to write code as you think of those decisions so you can validate them and avoid having to rewrite the entire thing.

There are several good principles you can follow, just google for "software principles" and you'll find a bunch of them.

Trade-offs: Everything is a trade-off. On one hand having too many dependencies is bad however you'll have to deal with extra complexity of managing dependencies somewhere else rather than in your domain objects. There's no right decision. If you have a gut feeling go for it. You'll learn a lot by taking that path and seeing the result.

Like I said, you're asking the right questions and that's the most important thing. Personally I agree with your reasoning but it looks you've already put too much time thinking about it. Just stop being afraid of making a mistake and make mistakes. They are really valuable and you can always refactor your code whenever you see fit.

Alex
  • 3,228
  • 1
  • 22
  • 25
  • Thank you for your answer. Since I posted that, I took **a lot** of paths. I tried AST, bound AST, free monads, now I’m trying to represent the whole thing through *typeclasses*. Indeed, there’s no absolute solution. I just feel the need to challenge the common way of doing relationship, which is to me far from being robust and flexible. – phaazon Jul 03 '14 at 16:42
  • @phaazon That's great and I wish more developers had a similar approach. Experimenting is the best learning tool. Good luck with your project. – Alex Jul 05 '14 at 11:50