Eryn Wells

Hugo's Dictionary API Development

Hugo’s templating system has support for dictionaries. Unfortunately the API for working with them is, frankly, awful. While working on developing some new templates for this site, I had to figure out how to build up dictionary data structures and it took me a long time to figure out how to do some basic operations with them.

Here’s a quick summary of what I found.

Creating Dictionaries

The function to create a dictionary is called dict and it takes a variable number of arguments that alternate between keys and values. It reminds me of this bizarre and backwards NSDictionary API in Apple’s Foundation framework. Keys must be strings (or string slices) and values can be anything. So this:

{{ $d := dict "a" 1 "b" 2 "c" 3 }}

creates a structure that looks like this JSON object:

{ "a": 1, "b": 2, "c": 3 }

You can also create an empty dictionary by calling dict with no arguments.

{{ $d := dict }}

Accessing Keys and Values

Statically, you can get a single item in a dictionary with dot syntax. Below, $item will get the value 1.

{{ $item := (dict "a" 1 "b" 2 "c" 3).a }}

If you want to get a value with a key you get at render time, you can use the index function. In the snippet below, $item will get the value of "b", which is 2.

{{ $key := "b" }}
{{ $item := index $key (dict "a" 1 "b" 2 "c" 3) }}

index doesn’t make much sense to me as a verb for accessing values in a dictionary. It sounds more like an array function, and indeed it’s the function that gives you access to items in arrays. I would like to see another function with a more dictionary-sounding name, like get or value or item, even if it were just an alias for index underneath.

Adding Items to a Dictionary

This is a bit complex because, as far as I can tell, dictionaries are immutable. So, if you want to update a dictionary, you need to combine two dictionaries and then save it back to the original variable. The merge function does that. Here’s a snippet:

{{ $d := dict "a" 1 "b" 2 "c" 3 }}
{{ $d = merge $d (dict "b" 4) }}
{{ $item = index "b" $d }}

merge takes a variable number of arguments, and merges dictionaries left to right. So, items in dictionaries later in the argument list will override items in dictionaries earlier in the list.

Just to underscore, you have to set the update dictionary back to the original variable to complete the update, hence the $d = ....

All that is to say: at the end of that snippet, $item will get the value 4.

A Complex Example: A Dictionary of Arrays

For the previously mentioned template changes I was making, I was updating the terms template for my category taxonomy. For each category, I wanted to show one section per tag, and a list of all the posts with that tag underneath.

My categories are high level groups like “Tech,” “Music,” and “Travel.” Tags are more specific topics for the post like “Web” or “Compositions.” Pages only ever have one category but they can have multiple tags.

A terms template lets you access an array of terms, and the pages associated with those terms. You can access the tags attached to a page with the .GetTerms function. Here’s what I did, and then I’ll talk through it:

{{- $pagesByTag := dict -}}
{{- range $page := .Pages -}}
  {{- range $tag := .GetTerms "tags" -}}
    {{- $tagName := $tag.Name -}}
    {{- if not (in $pagesByTag $tagName) -}}
      {{- $pagesByTag = merge $pagesByTag
          (dict $tagName (slice $page)) -}}
    {{- else -}}
      {{- $pagesForTag := index $pagesByTag $tagName -}}
      {{- $pagesForTag = $pagesForTag | append $page -}}
      {{- $pagesByTag = merge $pagesByTag
          (dict $tagName $pagesForTag) -}}
    {{- end -}}
  {{- end -}}
{{- end -}}

$pagesByTag is my empty dictionary. It will hold tag names as keys, each pointing to a slice (array) of page objects. For each page, I get its list of tags. For each tag, I check $pagesByTag to see if it already has a key/value pair for that tag. If not, I create a new entry in $pagesByTag with merge. If it does already, I get the slice for that tag with index, add the Page to the slice with append, and then merge the updated slice back into $pagesByTag with merge.

It’s not too bad once it’s all spelled out, but it does feel like more work than it should take for such simple operations.

I think this API could be improved substantially with some new functions that operate specifically on dictionaries and that have clear names that describe what they do.