4

I am working on a library to study game theoretic learning. In this setting, N agents are brought together and interact with an environment. Each agent derives a model of the interaction. The model of one agent depends on its N-1 opponents. I wrote the code determining that model for 1 agent and 2 agents, and am trying to generalize it. Here is part of the code I have:

data System w x a s h h' = System { signaling :: SignalingWXAS w x a s
                                  , dynamic   :: DynamicXAS x a s
                                  , strategy  :: MockupStrategy x a s h h' }

data JointState w x a s h h' = JointState { worldState  :: w
                                          , state       :: x
                                          , mockupState :: MockupState a s h h' }

systemToMockup :: ( Bounded w, Ix w, Bounded x, Ix x
                  , Bounded a, Ix a, Bounded s, Ix s
                  , Bounded (h a), Ix (h a), Bounded (h' s), Ix (h' s)
                  , History h, History h'
                  ) => System w x a s h h' -> Mockup a s h h'
systemToMockup syst = mock
    where
      mock z   = norm $ statDist >>=? condit z >>= computeStatesAndAction >>= sig >>=$ extractSignal
      statDist = ergodicStationary $ transition syst
      condit z = just z . mockupState
      sig      = uncurryN $ signaling syst
      strat    = strategy syst
      computeStatesAndAction joint = do
        let w = worldState joint
        let x = state joint
        a <- strat x (mockupState joint)
        return (w, x, a)
      extractSignal (_, s) = s

and

data System2 w x1 a1 s1 h1 h1' x2 a2 s2 h2 h2' = System2 { signaling :: SignalingWXAS2 w x1 a1 s1 x2 a2 s2
                                                         , dynamic1  :: DynamicXAS x1 a1 s1
                                                         , dynamic2  :: DynamicXAS x2 a2 s2
                                                         , strategy1 :: MockupStrategy x1 a1 s1 h1 h1'
                                                         , strategy2 :: MockupStrategy x2 a2 s2 h2 h2' }

data JointState2 w x1 a1 s1 h1 h1' x2 a2 s2 h2 h2' = JointState2 { worldState   :: w
                                                                 , state1       :: x1
                                                                 , mockupState1 :: MockupState a1 s1 h1 h1'
                                                                 , state2       :: x2
                                                                 , mockupState2 :: MockupState a2 s2 h2 h2' }
systemToMockups2 syst = (mock1, mock2)
    where
      mock1 z1   = norm $ statDist >>=? condit1 z1 >>= computeStatesAndActions >>= sig >>=$ extractSignal1
      mock2 z2   = norm $ statDist >>=? condit2 z2 >>= computeStatesAndActions >>= sig >>=$ extractSignal2
      statDist   = ergodicStationary $ transition2 syst
      condit1 z1 = just z1 . mockupState1
      condit2 z2 = just z2 . mockupState2
      sig        = uncurryN $ signaling syst
      strat1     = strategy1 syst
      strat2     = strategy2 syst
      computeStatesAndActions joint = do
        let w  = worldState joint
        let x1 = state1 joint
        let x2 = state2 joint
        a1 <- strat1 x1 (mockupState1 joint)
        a2 <- strat2 x2 (mockupState2 joint)
        return (w, x1, a1, x2, a2)
      extractSignal1 (_, s, _) = s
      extractSignal2 (_, _, s) = s

I am after a function definition for systemToMockupN that could accommodate any finite number of agents.

Agents are heterogenous so use of lists is not directly possible. I cannot use tuples because I do not know the size in advance. I tried using curryN, uncurryN, etc. but did not manage to do one operation on every element of a tuple. I tried building a variadic function in a fashion similar to printf with no success.

I know I could use template haskell but I am wondering if there is a nicer solution I am overlooking. Any pointer to some code out there dealing with a finite but arbitrary number of heterogenous elements would be greatly appreciated.

Nicolas Dudebout
  • 9,172
  • 2
  • 34
  • 43
  • 9
    The nicest solution to using heterogeneous collections is "don't". Presumably these agents interact with a common world environment. Can you represent an agent entirely in terms of its operations on that shared environment and/or operations on other agents using such a representation? That's the usual way to homogenize types like these. – C. A. McCann Sep 20 '12 at 14:29
  • This problem would be much easier if all agents had the same type. Can you explain why there are different types associated with each agent? – Heatsink Sep 20 '12 at 14:33
  • Here is one simple example. We want to model the agents participating in the so called smart grid. We have nuclear plants, coal plants, wind farms, data centers, environmentally friendly consumers. All these agents have very different dynamics, and different ways to impact the environment. – Nicolas Dudebout Sep 20 '12 at 14:38
  • C.A.McCann's suggestion is a very strong one: Can't you work with a generalisation using the idea type Agent = World -> Position -> (World -> World) or similar derivative thereof? – AndrewC Sep 20 '12 at 15:32
  • 1
    I have one homogenization concept. An agent is an entity that takes a signal as an input and generates an action. An environment is an entity taking `N` actions and generating `N` signals. An agent can work with any environment accepting the right action and producing the right signal. This is not fully heterogenous but the action and signal spaces are arbitrary finite sets. – Nicolas Dudebout Sep 20 '12 at 15:41
  • 1
    Also, if the types of agents are more easily constrained than their actions on the environment, you could also use the approach of consolidating them as constructors of a single data type, possibly using GADTs for extra fiddly type juggling. This merely shifts the problem from the collection to the "agents" type itself, which depending on what you need could be better or worse. – C. A. McCann Sep 20 '12 at 15:53
  • From what you say, my instinct is that shifting the issue to the actions and signals is the best way to start, then see what needs to be done to make that part work nicely. Unfortunately, questions like this are *really* hard to truly answer without seeing far more of your actual code than is practical on SO. – C. A. McCann Sep 20 '12 at 15:55
  • I suggest writing the type signatures out for your first few hypothetical systemToMockup functions so we can make suggestions more easily. – Gabriella Gonzalez Sep 20 '12 at 16:00
  • I had not put the type signatures because they rely on other types and carry with them quite a few constraints. I have now added the type signature of the first funtion. – Nicolas Dudebout Sep 20 '12 at 16:16
  • @C.A.McCann, I tried creating a smaller example with the same problem but always ended up with examples either trivial or not reflecting what I was after. For reference, the code is on github at the following address: https://github.com/dudebout/game-theoretic-learning/tree/master/GTL/Interaction. The two files I am trying to combine are called Consistency.hs and Consistency2.hs. – Nicolas Dudebout Sep 20 '12 at 16:23
  • I wasn't complaining about lack of detail, though the github links are helpful anyway. In fact, I probably could have told you that a smaller example wouldn't work. Really, I doubt a localized generic solution even exists to this kind of question, and that what you need is a larger-scale "meta-solution" that lets you reformulate things in a way that works more easily. (If it's not obvious, I've encountered similar situations in my own code...) – C. A. McCann Sep 20 '12 at 16:37

5 Answers5

3

Don't go heterogenous. It's not worth it. It is worth finding a better way. Here's one approach to avoiding it. (There are other paths.) There might be an arbitrary number of agents, but surely there aren't an arbitrary number of types of agents. (Do the types really need to be so parameterised? Your generality is costing you too much I fear.

    class AnAgent a where 
         liveABit :: World -> a -> (World,a)
         ...
         ...

    data NuclearPlant = ....
    data CoalPlant = ....
    data WidFarm = .....

    data DataCenter = .....

    data EnvFriendly = .....
    data PetrolHead = .....

Group them together a bit for common treatment via pattern matching if it's convenient:

    data Plant = PN NuclearPlant | PC CoalPlant | PW WindFarm
    data Human = HEF EnvFriendly | HPE PetrolHead

    data Agent = AP Plant | AH Human | AD DataCenter

Common/heterogenous treatment within groups/accross groups:

    bump :: Agent -> Agent
    bump (Human h) = Human (breakleg h)
    bump a = a

You can then define all the agents you want then pop them in a [Agent] and define a nice eachLiveABit :: World -> [Agent] -> (World,[Agent]) to update the world and its agents. You can make AnAgent instances of individual agent types or groups, and build up to Agent, (or maybe even do without the type class even and just use ordinary functions).

This would follow the (Classes + Interesting Type System Features) -> (Types and Higher Order Functions) program transformation that feels emotionally like your dumbing down a bit, but makes much of the trouble go.

Nicolas Dudebout
  • 9,172
  • 2
  • 34
  • 43
AndrewC
  • 32,300
  • 7
  • 79
  • 115
  • I thought about doing it this way but I was trying to write the code to be scenario agnostic. For example, another scenario I work with is some kind of market with different types of investors. I would have to redefine `data Agent` for this new scenario. It might work though, I will think about it. – Nicolas Dudebout Sep 20 '12 at 17:34
  • 1
    @NicolasDudebout: Would it be possible to usefully parameterize the rest of the code by the choice of `Agent` type for an approach like this? If so, that might work very nicely. – C. A. McCann Sep 20 '12 at 17:41
  • I am not sure I understand what the question is. – Nicolas Dudebout Sep 20 '12 at 17:45
3

Heterogenous. I don't think you should do this, but you did ask.

You can use en existing library for heterogenous lists, eg HList. This secretly uses Template Haskell anyway, but you don't need to get your hands as dirty as if you did it yourself.

Languages with dynamic typing have all sorts of problems because of the need for casting etc. HList is type-safe, but it's still not easy in my view to write code that works well and isn't buggy. You get an edge over tuples because you don't need to change type, and mapping your updates across elements should be easier, but it's still not pretty.

AndrewC
  • 32,300
  • 7
  • 79
  • 115
  • The original `HList` is more "historical artifact" than "useful library" these days. There are better ways to do the same thing. Also, I'm not sure what the TH dependency is for, because the core `HList` functionality doesn't use it. – C. A. McCann Sep 20 '12 at 17:46
  • @C.A.McCann: Then please please post about the better ways! I don't like this solution anyway! – AndrewC Sep 21 '12 at 06:02
  • The better ways involve a huge pile of GHC extensions added since the original version of `HList` was created, in some cases specifically to make it nicer to do things like `HList`. But I haven't actually tried using some of the newest stuff, so I can't really say much about them. – C. A. McCann Sep 21 '12 at 13:55
  • OK, so it's back to advice no.1: Don't do heterogenous lists. Experienced, knowledgable well-respected members of the community like @C.A.McCann avoid them; so should you. :) – AndrewC Sep 21 '12 at 14:05
  • Heterogenous lists do have their uses, to be sure. But for this question specifically I don't think they're the way to go. – C. A. McCann Sep 21 '12 at 14:10
2

Generalised Algebraic Data Types, (GADT).

These let you bring finitely many genuinely heterogenous data types together into one, and are the modern way to do existential types. They sit somewhere in between the data Agent = AH Human | AP Plant | .... approach and the HList approach. You can make all your incredibly heterogenous agents instances of some typeclass, then bundle them together in the AgentGADT. Make sure your typeclass has everything you'll ever want to do to an Agent in it, because it's hard to get data back out of a GADT without a function with an explicit type; will you need getHumans [AgentGADT] -> [Human]? or updateHumans :: (Human->Human) -> [AgentGADT] -> [AgentGADT]? That'd be easier with the ordinary abstract data type in my other post.

Plus points: You can have [AgentGADT] and operate uniformly using class functions, whilst writing weird and wonderfully parameterised data types. Minus points - hard to get your Agent data out once it's in.

My favourite introductory text online was GADTs for dummies.

AndrewC
  • 32,300
  • 7
  • 79
  • 115
0

You could try to solve that using type classes.
Here is some pseudo code:

data Mockup = Mockup1 <return type of systemToMockup>
            | Mockup2 <return type of systemToMockups2>

class SystemToMockup a where
    systemToMockup :: a -> Mockup

instance SystemToMockup (System1 ...) where
    <implementation of systemToMockup>

instance SystemToMockup (System2 ...) where
    <implementation of systemToMockups2>

This is a rather limited solution and I doubt it'll work for you since your application seems to be quite complex.
In general the approach of C. A. McCann is much better.

mmh
  • 275
  • 1
  • 7
  • I want to avoid having to rewrite the code for each number `N`. This solution allows me to factorize the calling but I still have to write `N` versions. – Nicolas Dudebout Sep 20 '12 at 15:38
0

Update: One thing this strategy cannot handle is polymorphic return types. Also, parameters that have different types depending on the main type add quite a bit of complexity.

It's been mentioned in the comments, but one of the best ways to do a "heterogeneous collection" is to decide what operations you can actually do on each element of the collection (since you'll only be able to do a restricted set of things on them, even if your language had duck typing) and store a record of those operations. Example:

data Thing1 = ...
data Thing2 = ...

data AThing = AThing {
  op1 :: ...
  op2 :: ...
}

class IsAThing a where
    mkAThing :: a -> AThing

instance IsAThing Thing1 where
    mkAThing a = AThing {
            op1 = op1_for_thing1 a,
            op2 = op2_for_thig1 a
        }

Then, to call op1 on any IsAThing:

op1 (mkAThing a) <...other args...>

Though, in your case you want a list of AThing:

[mkAThing a2, mkAThing a2, ...]

Then on any element:

op1 <element of that list> <...other args...>
singpolyma
  • 10,999
  • 5
  • 47
  • 71
  • The problem I have is the type `a`. This type varies for my different agents. – Nicolas Dudebout Sep 20 '12 at 20:42
  • @NicolasDudebout it varies in this example as well. I gave the example for Thing1, but the whole point of this approach is that the type can vary :) – singpolyma Sep 20 '12 at 20:47
  • But you have to give a type for `op1` and `op2`. My types will vary. – Nicolas Dudebout Sep 20 '12 at 21:00
  • @NicolasDudebout the types of arguments other than the one in the list and/or the return type will vary? Or are you thinking the type `a` of each of `Thing1`, `Thing2`, etc needs to be included? That type is not included, because the function is partially applied by the time you get that far. – singpolyma Sep 20 '12 at 21:09
  • `AThing` would need a type in my case because the operation is similar for all the agents but with a different type. – Nicolas Dudebout Sep 21 '12 at 00:54
  • @NicolasDudebout I think you're missing the whole point of this. The point is that if you have a set of functions which all take *different types* then you can partially apply them to the argument that is the *different type* and what is left is a function *of one type*. https://singpolyma.net/2012/08/heterogenous-collections-in-haskell/ – singpolyma Sep 21 '12 at 12:49
  • Or if you have one function *parametrically typed*, but you need to have only one type, so that you can put it into a data structure, this also works for that case. – singpolyma Sep 21 '12 at 12:50
  • In fact, if they types *weren't* different, there would be no point in doing this, since you could just make a list of them :) – singpolyma Sep 21 '12 at 12:52
  • My function `op1` is a strategy and has type `strat :: State -> Action -> Strategy` where `State`, `Action`, and `Strategy` are finite spaces that cannot be determined in the library. They are given by the scenario. In the example on your website you have fixed sets `Int` and `Bool`. – Nicolas Dudebout Sep 21 '12 at 13:18
  • @NicolasDudebout ah, you have polymorphic return type? Yes, that is one thing this cannot do. I thought I had mentioned that higher up, but I guess not. – singpolyma Sep 21 '12 at 13:22