How to model hierarchical data types in Haskell?

Question

I have a bunch of types where their hierarchy stores some useful information. I'm trying to avoid having to bake in knowledge of the hierarchy of the types into the functions that operate on them.

The following is a little excerpt of Stanford's Typed Dependencies for natural language processing:

root - root
dep - dependent
  aux - auxiliary
    auxpass - passive auxiliary 
    cop - copula
  arg - argument 
    agent - agent

I would like to create some data types that mirror this structure so that I can define some functions that can only operate on certain types. When I have a function that operates on an arg, the type that I use to represent arg should also include agent but the type for agent should not include arg. The type for dep should include anything below it.

Is this possible in haskell? I've been trying to declare various data types to model this, but I can't get it to work since a data type cannot use the constructor of another data type.

I suspect that maybe this approach doesn't work very well with Haskell, so if that's the case how do you usually deal with these cases where flattening the hierarchy is definitely undesirable?


Show source
| haskell   2017-01-06 12:01 2 Answers

Answers to How to model hierarchical data types in Haskell? ( 2 )

  1. 2017-01-06 12:01

    Use typeclasses.

    First define each of the concrete types as a seperate data declaration.

    Then for each type with subtypes declare a typeclass with a constraint on its parent. An example of this sort of relationship is Functor => Applicative => Monad structure in prelude.

    So to define the example structure:

    class Root where
        ...
    
    class Root => Dep where
        ...
    
    class Dep => Aux where
        ...
    
    class Aux => Auxpass where
        ...
    
    class Aux => Cop where
        ...
    
    class Dep => Arg where
        ...
    
    class Arg => Agent where
        ...
    
  2. 2017-01-06 18:01

    Subtyping in general does not play too well with Haskell. However, in the case that you are just trying to model (non-multiple) inheritance (so you have a tree of subtypes instead of a lattice), you can actually build sub typing up using type classes. Here is a short gist that does exactly this. Working from there, you define your data types

    data Root = Root ...
    data Dep = Dependent ...
    data Aux = Auxiliary ...
    data AuxPass = PassiveAuxiliary ... 
    data Cop = Copula ...
    data Arg = Argument ...
    data Agent = Agent ...
    

    And the corresponding instances

    instance Subtype Aux where
      type SuperType Aux = Dep
      embedImmediate = ...
    
    instance Subtype AuxPass where
      type SuperType AuxPass = Aux
      embedImmediate = ...
    
    instance Subtype Cop where
      type SuperType Cop = Aux
      embedImmediate = ...
    
    instance Subtype Arg where
      type SuperType Arg = Dep
      embedImmediate = ...
    
    instance Subtype Agent where
      type SuperType Agent = Arg
      embedImmediate = ...
    

    How you fill in the ... is up to you. A couple things you could do for this:

    • if your subtype adds lots of fields on top of the supertype, consistently, just add to it a field that has the supertype and make embedImmediate return that field
    • if your subtype adds only a couple of fields, you might want to manually unpack them. Your data definitions will look neater, but your embedImmediate definition will be a bit longer
    • if your subtypes doesn't add any fields to the supertype, you can just make a newtype around the super type and embedImmediate = coerce (from Data.Coerce)

    Then, you can't quite just use the subtypes in functions that expect supertypes, but almost: you just have to add a call to embed (different from embedImmediate!) to convert from the subtype to whichever supertype is needed (based on type inference). You may want to check out some example uses.

    Note that you can now use <: as a constraint: the type of something that is a subtype of Aux, for example, is (a <: Aux) => a. Whenever you want this thing treated as an Aux (or a super type of Aux), call embed on it.

    The one big downside of this approach is that the input and output types must be annotated (else it isn't clear which supertype you want to embed into and you'll get "ambiguous type" errors). If you already write lots of signatures, you should be fine though.

Leave a reply to - How to model hierarchical data types in Haskell?

◀ Go back