Click here to Skip to main content
15,867,867 members
Articles / Programming Languages / Python2.7

Down the Rabbit Hole with Array of Generics

Rate me:
Please Sign up or sign in to vote.
4.83/5 (29 votes)
4 May 2016CPOL6 min read 30.1K   123   30   13
An Alice in Wonderland journey of generics, inverting object oriented programming, and generic type dispatching

The Problem

Image 1

I recently had the odd requirement for creating an array of different generic types, in which I could call methods that would operate on the concrete type. Basically, what I wanted to accomplish would look sort of like this (non-working example):

C#
public class Cat
{
  public void Meow() {Console.WriteLine("Meow");}
}

public class Dog
{
  public void Bark() {Console.WriteLine("Woof");}
}

public static class BasicExample
{
  static void Test()
  {
    object[] msgs = new object[]
    {
      new Cat(),
      new Dog(),
    };

    DoSomethingWith(msgs[0]);
    DoSomethingWith(msgs[1]);
  }

  static void DoSomethingWith(Cat cat)
  {
    cat.Meow();
  }

  static void DoSomethingWith(Dog dog)
  {
    dog.Bark();
  }
}

Ignore the fact that the usual way we implement the above example is through a common interface that implements something like Speak(). In my particular case, I needed an array of concrete types whose properties and methods vary, and where I could call an externally defined method that does some specific operation with the concrete type instance, qualified by some filtering on the properties of the instance.

The above code doesn't work because in DoSomethingWith(msgs[0]); the parameter, msgs[0] is of type object, not of the exact type initialized in the array. (For the advanced reader, my experiments with co- and contra-variance didn't lead anywhere.)

So I thought, let's do this with generics. All I need is an array of generics, like this:

C#
public class Animal<T> { }
C#
Animal<T>[] msgs = new Animal<T>[]
{
  new Animal<Cat>(),
  new Animal<Dog>(),
};

Oops, of course that doesn't work because the l-value Animal<T>[] msgs is not a known type -- T must be a defined type.

So a quick Google led me to an amusing post on Stack Overflow (here) where the response was: "It's not possible."

That was not acceptable!

Down the Rabbit Hole we Go!

Image 2

The solution is rather simple -- we have to create a generic type (we already did, Animal<T>) but derive it from a non-generic type:

C#
public abstract class Animal{ }

public class Animal<T> : Animal { }

Now, the array can be of type Animal :

C#
Animal[] animals = new Animal[]
{
  new Animal<Cat>(),
  new Animal<Dog>(),
};

But the DoSomethingWith call is still not correct, because the type is now Animal.

The solution to this is to invert the call, so that DoSomethingWith is called by the generic Animal<T> class, because it's there that we know what type T is.

This requires an Action:

C#
public class Animal<T> : Animal 
{
  public Action<T> Action { get; set; }
}

We now initialize the action when we construct the specific generic type:

C#
Animal[] animals = new Animal[]
{
  new Animal<Cat>() {Action = (t) => DoSomethingWith(t)},
  new Animal<Dog>() {Action = (t) => DoSomethingWith(t)},
};

But how do we invoke the action? The secret sauce is in an abstract Call method in the non-generic Animal that is implemented in the generic Animal<T> class:

C#
public abstract class Animal
{
  public abstract void Call(object animal);
}

public class Animal<T> : Animal 
{
  public Action<T> Action { get; set; }

  public override void Call(object animal)
  {
    Action((T)animal);
  }
}

Note also the cast to T in the call to Action!

Now, this works:

C#
public static class BasicConcept3
{
  public static void Test()
  {
    Animal[] animals = new Animal[]
    {
      new Animal<Cat>() {Action = (t) => DoSomethingWith(t)},
      new Animal<Dog>() {Action = (t) => DoSomethingWith(t)},
    };

    animals[0].Call(new Cat());
  }

  static void DoSomethingWith(Cat cat)
  {
    cat.Meow();
  }

  static void DoSomethingWith(Dog dog)
  {
    dog.Bark();
  }
}

Image 3

What's going on here?

Image 4

 

But Wait, This is Stupid!

Image 5

If I'm already instantiating Cat and Dog, why not just do this:

C#
DoSomethingWith(new Cat());
DoSomethingWith(new Dog());

Because the code in the previous section was just an example to illustrate the implementation. The real goal here is to be able to receive an Animal, through, say, an event:

C#
public class SpeakEventArgs : EventArgs
{
  public IAnimal Animal { get; set; }    // Could have been object Animal as well.
}

public static EventHandler<SpeakEventArgs> Speak;

Image 6

Here, we are creating a separation of concerns -- the event receives some object, and we'll figure out how to route that object to the desired handler.

You'll notice that I snuck in an interface IAnimal. This isn't technically necessary, the property Animal could also simply be an object, but this ensures that we create the SpeakEventArgs with a type that implements the interface:

C#
public class Cat : IAnimal
{
  public void Meow() { Console.WriteLine("Meow"); }
}

public class Dog : IAnimal
{
  public void Bark() { Console.WriteLine("Woof"); }
}

Now, our implementation requires a way to select the "route", which means that we also have to expose the type of T in order to qualify the route. Back to the generic and non-generic classes, where we add a property Type.

C#
public abstract class Animal
{
  public abstract Type Type { get; }
  public abstract void Call(object animal);
}

public class Animal<T> : Animal
{
  public override Type Type { get { return typeof(T); } }
  public Action<T> Action { get; set; }

  public override void Call(object animal)
  {
    Action((T)animal);
  }
}

Now we can implement a dispatcher:

C#
public static void OnSpeak(object sender, SpeakEventArgs args)
{
  animalRouter.Single(a => args.Animal.GetType() == a.Type).Call(args.Animal);
}

The full class looks like this now (all these statics are just a convenience, there's nothing preventing you from removing the static keywords and instantiating the class):

C#
public static class EventsConcept
{
  public static EventHandler<SpeakEventArgs> Speak;

  private static Animal[] animalRouter = new Animal[]
  {
    new Animal<Cat>() {Action = (t) => DoSomethingWith(t)},
    new Animal<Dog>() {Action = (t) => DoSomethingWith(t)},
  };

  static EventsConcept()
  {
    Speak += OnSpeak; 
  }

  public static void OnSpeak(object sender, SpeakEventArgs args)
  {
    animalRouter.Single(a => args.Animal.GetType() == a.Type).Call(args.Animal);
  }

  static void DoSomethingWith(Cat cat)
  {
    cat.Meow();
  }

  static void DoSomethingWith(Dog dog)
  {
    dog.Bark();
  }
}

Somewhere else in the program, the event can now be fired:

C#
EventsConcept.EventsConcept.Speak(null, new EventsConcept.SpeakEventArgs() 
{ Animal = new EventsConcept.Cat() });
EventsConcept.EventsConcept.Speak(null, new EventsConcept.SpeakEventArgs() 
{ Animal = new EventsConcept.Dog() });

Image 7

Again you say, but this is stupid! I could just do:

C#
EventsConcept.EventsConcept.DoSomethingWith(new EventsConcept.Cat());
EventsConcept.EventsConcept.DoSomethingWith(new EventsConcept.Dog());

Yes, of course, but that assumes that the publisher of the animal knows what to do with it. In the previous eventing implementation, we publish the animal, and it is the subscriber that figures out what to do.

Image 8

What we've done here is turn object oriented programming on its head -- we're essentially implementing, via the dispatcher, what goes on behind the scenes in OOP dynamic dispatching.

A Better Example -- Filtering

What I really needed was a way to dispatch (aka route) the objects based on the values of one or more fields. For example:

C#
public class Cat : IAnimal
{
  public bool IsSiamese {get; set;}
}

public class Dog : IAnimal
{
  public bool IsRotweiler {get;set;}
}

Notice that I also removed the Speak method, because we want the "computation" on the concrete Animal type to be decoupled from the Animal instance (as well as the publisher.)

This requires adding to our generic and non-generic instance management classes an abstract Where method and a Func<T> for implementing a filter expression:

C#
public abstract class Animal
{
  public abstract Type Type { get; }
  public abstract void Call(object animal);
  public abstract bool Where(object animal);
}

public class Animal<T> : Animal
{
  public override Type Type { get { return typeof(T); } }
  public Action<T> Action { get; set; }
  public Func<T, bool> Filter { get; set; }

  public override void Call(object animal)
  {
    Action((T)animal);
  }

  public override bool Where(object animal)
  {
    return animal is T ? Filter((T)animal) : false;
  }
}

Notice that the implementation for Where also checks the type -- if we don't do this, we'll get a runtime error when executing the filter.

Our test class now looks like this:

C#
public static class FilteredEventsConcept
{
  public static EventHandler<SpeakEventArgs> Speak;

  private static Animal[] animalRouter = new Animal[]
  {
    new Animal<Cat>() {Filter = (t) => t.IsSiamese, 
    Action = (t) => Console.WriteLine("Yowl!")},
    new Animal<Cat>() {Filter = (t) => !t.IsSiamese, 
    Action = (t) => Console.WriteLine("Meow")},
    new Animal<Dog>() {Filter = (t) => t.IsRotweiler, 
    Action = (t) => Console.WriteLine("Growl!")},
    new Animal<Dog>() {Filter = (t) => !t.IsRotweiler, 
    Action = (t) => Console.WriteLine("Woof")},
  };

  static FilteredEventsConcept()
  {
    Speak += OnSpeak;
  }

  public static void OnSpeak(object sender, SpeakEventArgs args)
  {
    animalRouter.Single(a => a.Where(args.Animal)).Call(args.Animal);
  }
}

and our test like this:

C#
FilteredEventsConcept.TestCase.Test();

// So we stay in the same namespace and our test is easier to read:
public static class TestCase
{
  public static void Test()
  {
    FilteredEventsConcept.Speak(null, new SpeakEventArgs() { Animal = new Cat() });
    FilteredEventsConcept.Speak(null, new SpeakEventArgs() { Animal = new Cat() { IsSiamese = true } });
    FilteredEventsConcept.Speak(null, new SpeakEventArgs() { Animal = new Dog() });
    FilteredEventsConcept.Speak(null, new SpeakEventArgs() { Animal = new Dog() { IsRotweiler = true } });
  }
}

Image 9

Now we've accomplished something useful -- our dispatcher not only dispatches based on type, but also allows us to qualify the action with some filter on the type instance's data. All this, simply to replace:

C#
if (animal is Cat && ((Cat)animal).IsSiamese) Console.WriteLine("Yowl!");

etc. But I dislike using imperative code when there is a debatable, more complex, declarative solution!

Minor Refactoring

Because this is a generic router, we should really rename the two classes that support routing and change the animal parameter name simply obj:

C#
public abstract class Route
{
  public abstract Type Type { get; }
  public abstract void Call(object obj);
  public abstract bool Where(object obj);
}

public class Route<T> : Route
{
  public override Type Type { get { return typeof(T); } }
  public Action<T> Action { get; set; }
  public Func<T, bool> Filter { get; set; }

  public override void Call(object obj)
  {
    Action((T)obj);
  }

  public override bool Where(object obj)
  {
    return obj is T ? Filter((T)obj) : false;
  }
}

Duck Typing

Image 10

In Python, we can do something similar:

Python
class Cat(object):
  def __init__(self):
    self.isSiamese = False

class Dog(object):
  def __init__(self):
    self.isRotweiler = False 

class Route(object):
  def __init__(self, typeCheck, filter, do):
    self.__typeCheck = typeCheck
    self.__filter = filter
    self.__do = do

  def where(self, obj):
    return self.__isType(obj) and self.__filter(obj)

  def do(self, obj):
    self.__do(obj)

  # Attributes and functions with a two leading underscore is the pythonic way of 
  # indicating the method is supposed to be "private", as this "scrambles" the name.
  def __isType(self, obj):
    return self.__typeCheck(obj)

  def __filter(self, obj):
    return self.__filter(obj)

router = [
  Route(lambda animal : type(animal) is Cat, 
  lambda animal : animal.isSiamese, lambda animal : speak("Yowl!")),
  Route(lambda animal : type(animal) is Cat, 
  lambda animal : not animal.isSiamese, lambda animal : speak("Meow")),
  Route(lambda animal : type(animal) is Dog, 
  lambda animal : animal.isRotweiler, lambda animal : speak("Growl!")),
  Route(lambda animal : type(animal) is Dog, 
  lambda animal : not animal.isRotweiler, lambda animal : speak("Woof"))
]
Python
def speak(say):
  print(say)

def dispatcher(animal):
  filter(lambda route : route.where(animal), router)[0].do(animal)

cat1 = Cat()
cat2 = Cat()
cat2.isSiamese = True

dog1 = Dog()
dog2 = Dog()
dog2.isRotweiler = True

dispatcher(cat1)
dispatcher(cat2)
dispatcher(dog1)
dispatcher(dog2)

Image 11

The salient point to the Python code is this:

C#
router = [
  Route(lambda animal : type(animal) is Cat, 
	lambda animal : animal.isSiamese, lambda animal : speak("Yowl!")),
  Route(lambda animal : type(animal) is Cat, 
	lambda animal : not animal.isSiamese, lambda animal : speak("Meow")),
  Route(lambda animal : type(animal) is Dog, 
	lambda animal : animal.isRotweiler, lambda animal : speak("Growl!")),
  Route(lambda animal : type(animal) is Dog, 
	lambda animal : not animal.isRotweiler, lambda animal : speak("Woof"))
]

Here, the lambda expression lambda animal : animal.isSiamese is duck typed. It doesn't need to know the type at "compile time" (because there isn't any compile time) in order to evaluate the lambda expression. Conversely, in C#, intellisense already knows the type because t can only be of type Cat:

Image 12

The only way in Python to know that you haven't screwed up:

Python
Route(lambda animal : type(animal) is Cat, 
lambda animal : animal.isRotweiler, lambda animal : speak("Yowl!")),

is to run (or preferably write a unit test) the code:

Image 13

C# Duck Typing

Technically, we have the same problem with our C# code. Consider this simpler example:

C#
public abstract class SomeMessage
{
  public abstract void Call(object withMessage);
}

public class Message<T> : SomeMessage
{
  public Action<T> Action { get; set; }

  public override void Call(object withMessage)
  {
    Action((T)withMessage);
  }
}

public class CallbackExamplesProgram
{
  public static void Test()
  {
    SomeMessage[] messages = new SomeMessage[]
    {
      new Message<MessageA>() {Action = (msg) => MessageCallback(msg)},
      new Message<MessageB>() {Action = (msg) => MessageCallback(msg)}
    };

  try
  {
    // Cast error caught at runtime:
    messages[0].Call(new MessageB());
  }
  catch (Exception ex)
  {
    Console.WriteLine(ex.Message);
  }
}

Here, we calling the Action for the first message of type MessageA but passing in a MessageB instance. We get, at runtime (not at compile time!):

Image 14

which is why, in the filtered event example earlier, the Where method checks the type:

C#
public override bool Where(object obj)
{
  return obj is T ? Filter((T)obj) : false;
}

Visitor Pattern

Image 15

As ND Hung pointed out in the comments, the Visitor Pattern is good OOP solution to this problem.  A bare bones example would look like this:

namespace VisitorPatternExample
{
  public interface IVisitor
  {
    void Visit(Cat animal);
    void Visit(Dog animal);
  }

  public interface IAnimal
  {
    void Accept(IVisitor visitor);
  }

  public class Cat : IAnimal
  {
    public void Accept(IVisitor visitor)
    {
      visitor.Visit(this);
    }
  }

  public class Dog : IAnimal
  {
    public void Accept(IVisitor visitor)
    {
      visitor.Visit(this);
    }
  }

  public class Visitor : IVisitor
  {
    public void Visit(Cat cat)
    {
      Console.WriteLine("Cat");
    }

    public void Visit(Dog dog)
    {
      Console.WriteLine("Dog");
    }
  }

  public static class VisitorTest
  {
    public static void Test()
    {
      IAnimal cat = new Cat();
      IAnimal dog = new Dog();

      Visitor visitor = new Visitor();
      cat.Accept(visitor);
      dog.Accept(visitor);
    }
  }
}

Image 16

Declarative vs. Imperative, and Use Case Analysis

The visitor pattern avoids the whole issue with array of generics, which is what I was aiming to solve.  However, this points out that we may think that the use-case (at least in my examples) requires an array of generics, but in reality, something as simple as the visitor pattern can solve the problem without turning OOP on its head and creating complex solutions with generic arrays.  It also reveals the difference (and complexity that one gets into) with declarative vs. imperative code.  With the visitor pattern, the filtering must be implemented imperatively:

public void Visit(Cat cat)
{
  if (cat.IsSiamese)
    Console.WriteLine("Yowl!");
  else
    Console.WriteLine("Meow.");
}

public void Visit(Dog dog)
{
  if (dog.IsRotweiler)
    Console.WriteLine("Growl!");
  else
    Console.WriteLine("Woof");
  }
}

This of course is a perfectly acceptable solution.  Conversely, with the declarative solution, we don't need an IVisitor (or similar) interface, so when new message types are added, only the declarative router is changed.  In the visitor pattern, the interface must be touched to add a Visit method for the new type.  The tradeoff is complexity in implementing a declarative approach vs. the simplicity of the imperative approach.

Conclusion

This was originally intended to be posted as a tip, but it grew a bit too large!  The visitor pattern doesn't demonstrate how to work with an array of generics (which was the point of this article) but it is a good example of how design patterns can be used to solve the use case (which after all doesn't require an array of generics) and the tradeoffs between imperative and declarative programming..

The source code contains the examples in this article plus a few more.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
GeneralMy vote of 5 Pin
raddevus27-Mar-17 10:40
mvaraddevus27-Mar-17 10:40 
PraiseGreat read Pin
Member 1251708611-May-16 2:45
Member 1251708611-May-16 2:45 
QuestionObserver Pattern : Readable / maintainable version Pin
Ed Nutting6-May-16 13:39
Ed Nutting6-May-16 13:39 
You seem to have re-created the Observer pattern without realising it (and in a less readable, less maintainable, less convenient way).

E.g.

C#
using System;
using System.Collections.Generic;

namespace ObserverPattern
{
    /// <summary>
    /// Note: Not a necessary class but is a useful wrapper
    /// </summary>
    class ObserverList
    {
        private List<Observer> Observers = new List<Observer>();

        public bool SingleObserver
        {
            get;
            set;
        }

        public void Register(Observer AnObserver)
        {
            Observers.Add(AnObserver);
        }
        public void Notify(object Subject)
        {
            foreach (Observer AnObserver in Observers)
            {
                if (AnObserver.Accepts(Subject))
                {
                    AnObserver.Notify(Subject);

                    if (SingleObserver)
                    {
                        break;
                    }
                }
            }
        }
    }
    abstract class Observer
    {
        public abstract bool Accepts(object x);
        public abstract void Notify(object Subject);
    }
    abstract class Observer<T> : Observer
    {
        public override bool Accepts(object x)
        {
            return x is T;
        }
        public override void Notify(object Subject)
        {
            Notify((T)Subject);
        }
        public abstract void Notify(T Subject);
    }
    class DogObserver : Observer<Dog>
    {
        public override void Notify(Dog Subject)
        {
            if (Subject.IsRotweiler)
            {
                Subject.Growl();
            }
            else
            {
                Subject.Bark();
            }
        }
    }
    class RotweilerObserver : DogObserver
    {
        public override bool Accepts(object x)
        {
            return x is Dog && ((Dog)x).IsRotweiler;
        }
        public override void Notify(Dog Subject)
        {
            Console.WriteLine("Rotweiler...AAAH!");
        }
    }
    class CatObserver : Observer<Cat>
    {
        public override void Notify(Cat Subject)
        {
            if (Subject.IsSiamese)
            {
                Subject.Growl();
            }
            else
            {
                Subject.Meow();
            }
        }
    }
    /// <summary>
    /// Note: This base class is not required, it's just used to show the power and simplicity of this model.
    /// </summary>
    class Animal
    {
        public void Growl()
        {
            Console.WriteLine("Grr!");
        }
    }
    class Dog : Animal
    {
        public bool IsRotweiler
        {
            get;
            set;
        }

        public void Bark()
        {
            Console.WriteLine("Woof!");
        }
    }
    class Cat : Animal
    {
        public bool IsSiamese
        {
            get;
            set;
        }

        public void Meow()
        {
            Console.WriteLine("Meow!");
        }
    }
    class Mutant
    {
    }


    class Program
    {
        static ObserverList ManyOberserverList = new ObserverList()
        {
            SingleObserver = false
        };
        static ObserverList SingleOberserverList = new ObserverList()
        {
            SingleObserver = true
        };

        static void Main(string[] args)
        {
            ManyOberserverList.Register(new DogObserver());
            ManyOberserverList.Register(new RotweilerObserver());
            ManyOberserverList.Register(new CatObserver());

            // Note: Order matters for single-observer
            //  unless you guarantee no two observers will accept the same object
            //      See below: You could add a method to your ObserverList to check for multiple-acceptance
            SingleOberserverList.Register(new RotweilerObserver());
            SingleOberserverList.Register(new DogObserver());
            SingleOberserverList.Register(new CatObserver());

            // Some time later in the program...

            ManyOberserverList.Notify(new Dog()
            {
                IsRotweiler = true
            });
            ManyOberserverList.Notify(new Dog()
            {
                IsRotweiler = false
            });
            ManyOberserverList.Notify(new Cat()
            {
                IsSiamese = true
            });
            ManyOberserverList.Notify(new Cat()
            {
                IsSiamese = false
            });


            SingleOberserverList.Notify(new Dog()
            {
                IsRotweiler = true
            });
            SingleOberserverList.Notify(new Dog()
            {
                IsRotweiler = false
            });
            SingleOberserverList.Notify(new Cat()
            {
                IsSiamese = true
            });
            SingleOberserverList.Notify(new Cat()
            {
                IsSiamese = false
            });

            // But of course the above could be literally any object...including null instances
            //  ...and they have no effect on the rest of the system!
            // And if you decide to add a MutantObserver, it requires no changes to any of the other classes!
            //  Which is what you were aiming for.

            ManyOberserverList.Notify(null);
            ManyOberserverList.Notify(new Mutant());

            SingleOberserverList.Notify(null);
            SingleOberserverList.Notify(new Mutant());

            // And you could, of course, add something to the generic observer list as a catch-all case e.g.
            //  to throw an error if no observer accepts the object/event or do some generic action for all cases.

            // And so on and so forth because if you bring in further use of interfaces, abstract classes, delegates etc. 
            //  you get a very extensible, easy-to-read, flexible but usefully sparse-dependency-network of classes

            Console.ReadKey();
        }
    }
}


Which is a fair bit cleaner/more readable, more powerful, more re-usable and more future proof than what you've demonstrated above? Typically the observer pattern uses a common interface or base class for the objects being observed, but above I've implemented a more generic, more flexible version. A tad slower than if you have a common base class or interface but still plenty good enough.

This isn't tremendously different from what you've presented above and relies on similar principles. It's just a much cleaner version that has a few subtle improvements. Of course we note that the object passed in could be an event or more than one thing - that's just expanding/change the arguments t whatever fits your needs.

Best,
Ed
GeneralRe: Observer Pattern : Readable / maintainable version Pin
Ed Nutting6-May-16 13:54
Ed Nutting6-May-16 13:54 
GeneralRe: Observer Pattern : Readable / maintainable version Pin
Marc Clifton11-May-16 5:40
mvaMarc Clifton11-May-16 5:40 
QuestionMeh on generics... Pin
Sander Rossel6-May-16 12:35
professionalSander Rossel6-May-16 12:35 
AnswerPerhaps a easier way of doing that Pin
Member 78703455-May-16 22:51
professionalMember 78703455-May-16 22:51 
GeneralRe: Perhaps a easier way of doing that Pin
Marc Clifton6-May-16 6:29
mvaMarc Clifton6-May-16 6:29 
GeneralMy vote of 5 Pin
Jim Meadors5-May-16 19:23
Jim Meadors5-May-16 19:23 
SuggestionVisitor pattern Pin
ND Hung4-May-16 23:05
ND Hung4-May-16 23:05 
GeneralRe: Visitor pattern Pin
Marc Clifton5-May-16 3:08
mvaMarc Clifton5-May-16 3:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.