Sunday, 20 May 2018

#PeanutButter.Utils: the dictionaries

Dictionaries, HashMaps, whatever you want to call them -- they can be one of the most useful constructs in any language. In Javascript, the dictionary interface to objects makes a lot of dynamic code simpler. In fact, it was the Javascript paradigm which served as the inspiration for `PeanuButter.Utils` member: `DictionaryWrappingObject`. This class was originally made to facilitate some of the functionality in `PeanutButter.Ducktyping`: a library for duck-typing arbitrary objects onto interfaces which aren't directly related. For example, if you had an anonymous object with the correct "shape", you could duck-type it onto an interface which another part of the system requires without having to manually create your own implementation of that interface and the code to copy data / forward method calls -- all of which `PeanutButter.DuckTyping` can do, with varying amounts of flexibility to matching methods and properties, according to your needs.

But I'm not here to talk about `PeanutButter.DuckTyping` today -- as interesting as it was to write and as useful as it's proven to be in a couple of projects since then.

So, back to `DictionaryWrappingObject`: this class provides the familiar `IDictionary<string, object>` interface over any other object so you can enumerate through the properties or perform functions like querying property names without directly using reflection yourself. And, of course, you can just use very Javascript-y syntax:
var obj = new { id = 1, name = "bob" };
var wrapper = new DictionaryWrappingObject(obj);
var name = wrapper["name"];
You can also construct with a case-insensitive StringComparer to make those lookups a little fuzzier:
var obj = new { Id = 1, Name = "Mary" };
var wrapper = new DictionaryWrappingObject(obj);
var id = wrapper["ID"];
var name = wrapper["name"];

Another useful dictionary construct that I stumbled across in Python is the default dictionary -- implemented in `PeanutButter.Utils`, unsurprisingly, as `DefaultDictionary`. This dictionary allows you to specify a default value to return for the case where requested keys aren't found:
var animalsInZoo = new DefaultDictionary<string, bool>(false);
animalsInZoo["Camel"] = true;
animalsInZoo["Panda"] = true;

//... some time later:
var haveCamels = animalsInZoo["Camel"]; // true
var haveSnakes = animalsInZoo["Snake"]; // false, not KeyNotFoundException!

`DefaultDictionary` can be a little smarter than having a static value for the default:
// set up the default dictionary such that students with a name starting
// "A" exist.
var students = new DefaultDictionary<string, bool>(
  k => k.StartsWith("A")
students["Anna"] = false;
students["Mary"] = true;
// ... elsewhere ...

var haveAnna = students["Anna"]; // false, explicitly set
var haveAndrew = studentds["Andrew"]; // true: default value provider
var haveMary = students["Mary"]; // true: explicitly set
var haveStewart = students["Stewart"]; // false: default value provider

`DefaultDictionary` is expecially useful in conjunction with `MergeDictionary`, which takes one or more other dictionaries with the same key/value types and merges them, returning the value from the first in the merge list to have a value. So we could have:
var config = new Dictionary<string, string>()
  ["host"] = "database-machine",
  ["port"] = "123"
var defaults = new Dictionary<string, string>(k =>
  switch (k):
    case "host":
      return "localhost";
    case "port":
      return "3306";
    case "user":
      return "mysql";
    case "password":
      return "super-secret";
      return "";
var final = new MergeDictionary<string, string>(
  config, defaults

// ... elsewhere ...
var config = new 
  host: final["host"],        // database-machine
  port: int.Parse(final["port"]), // 123
  user: final["user"],        // mysql (default)
  password: final["password"] // super-secret (default)


Finally, there is the `CaseWarpingDictionary`, which basically acts as a wrapper for another dictionary to change the case-sensitivity of the keys, especially useful when you want a dictionary that is a little more forgiving (case-insensitive) than the one you're working with. `CaseWarpingDictionary` can be constructed with either a boolean instructing whether or not the result is case-sensitive, or with a StringComparer, so you can, for instance, switch from `Ordinal` to `CurrentCultureIgnoreCase`.

## Last words
The `IDictionary<TKey, TValue>` interface is not particularly difficult to implement, but it is quite convenient to consume. And I recommend devs wanting to learn something about the internals of .NET to implement some kind of `IDictionary<TKey, TValue>` at some point in their lives. For one thing, it will give you an appreciation for the good old `Dictionary` (:

No comments:

Post a Comment

What's new in PeanutButter?

Retrieving the post... Please hold. If the post doesn't load properly, you can check it out here: