Functional Inventory Design

I'd like to write a story game in Elm, along the lines of my previous browser game 5:28. However I'd like to design it with a modern frontend framework, rather than stringing together a hodgepodge of jQuery animation calls. I'm also deliberately choosing a functional language so I have to think particularly carefully about data types.

Primarily the player will have some form of inventory. We'll represent this as a list of stacks of items – much like Minecraft, we're assuming players will acquire more than one of some item, and want them to be collapsed into a single listing.

type alias Inventory = 
    List ItemStack

We'll define ItemStack as a grouping of identical items, so information about one item is true for all of them:

type alias ItemStack =
    { item : ItemInfo
    , count : Int 
    }

Although this will likely be subject to change, we can make some assumptions about what the ItemInfo might comprise.

type alias ItemInfo =
    { name : String
    , description : String
    , qualities : List ItemQuality
    }

Instead of restricting the ItemInfo to a set category, we'll allow it to have any number of ItemQualities. The Qualities system – terminology borrowed from Fallen London – lets us represent any information about the item in an unconstrained way. I've used a List here although item qualities don't have any particular default ordering; we might at some point wish to order the qualities by relevance, but in general we just need to check for membership or non-membership, intersections and unions. Sets should be perfect for this, but unfortunately the standard library doesn't offer as many tools for set iteration as it should, and the only methods for converting to lists involve sorting the elements (seriously!).

type alias itemQuality =
    { name : String
    , value : ItemQualityValue
    }

An ItemQuality is a key-value pair. The key is always a string – "stolen", "street market price" or "shiny", for example – and the value can either be a number, a boolean or a string. The string could be anything from a previous owner's name to a qualitative description like "antique".

type ItemQualityValue
    = Number Int
    | Text String
    | Boolean Bool

Here is where the buck stops as far as type safety is concerned. I could go deeper, encapsulating more information about the ItemQuality in the type system, but at this point I believe it'd just be diminishing returns. The quality of error messages the compiler gives are so high that, coupled with the time-travelling debugger, any further typing at this point will probably just make things harder to understand and trace through.

I've also chosen not to try anything clever with the type system like calling the boolean field Is in order to get slightly nicer parsing messages. In my experience the best choice is always just to work with the type system as is, without trying to use English-language hacks to make things slightly more readable. It's only more confusing in the long run!

With these types defined, we can start writing helper functions like this one to combine two stacks of items.

addStacks : ItemStack -> ItemStack -> ItemStack
addStacks oldStack newStack =
    let
        newCount =
            oldStack.count + newStack.count
    in
        { oldStack | count = newCount }

We can then use this to add an item stack to an existing inventory!

addStackToInventory : ItemStack -> Inventory -> Inventory
addStackToInventory newStack inventory =
    case inventory of
        [] ->
            [ newStack ]

        stack :: stacks ->
            if (stack.item.name == newStack.item.name) then
                (addStacks newStack stack) :: stacks
            else
                [ stack ] ++ addStackToInventory newStack stacks