Dict constructor methods in Julia

Mar 20, 2018 · 785 words · 4 minutes read juliatypes

One of the great features of Julia is that it has a strong type system. So while I was working on DarkSky.jl one of the things I wanted was to create a composite type for the API response (rather than leaving the response in a Dict or as an unparsed string). The response from the Dark Sky API is formatted as JSON, which I then parsed to a Dict. However I wanted to find an elegant solution to convert the Dict to the composite type.

Composite types

There are a lot of scenarios where you might find yourself wanting to convert a Dict to a composite type – loading JSON from an API is just one of them. Before we dive into my recipe for how to create, let’s review Julia’s composite types.

Here is an example of a composite type definition for Foo, which has two fields, bar and qux.

struct Foo
    bar::String
    qux::String
end

In this example both bar and qux must be strings and we can define an instance of Foo by providing values for the fields.

myfoo = Foo("hello", "world")
isa(myfoo, Foo)

Suppose we also have a Dict with keys that match the fieldnames of Foo:

mydict = Dict("bar" => "hello", "qux" => "world")

Then it is natural to want to construct an instance of our composite type. That would look like this:

Foo(mydict)

But this method doesn’t exist (yet).

Recipe for a Dict argument constructor method

We want a method to turn our Dict, mydict, into an instance of our composite type, Foo. Constructor methods are just functions that create new objects of our composite type.

Here is how we can define the new constructor method:

function Foo(x::Dict)
    Foo((get.(x, String.(fieldnames(Foo)), nothing))...)
end

This method tries to get the stored value of the Dict for each field name of Foo. If the Dict doesn’t contain the field name, it returns nothing.

Note: I’m using the ... operation to unpack the dict. You might be tempted to use Foo(values(mydict)...) if you know your Dict contains only keys that correspond to the composite type field names but can run into problems if the Dict keys are not ordered the same as the composite type or contains any extra keys.

While this looks great there is one slight problem; nothing is not a string! So if our Dict doesn’t have both bar and qux fields this method will raise a type error. Fortunately we can use a trick to circumvent this: type unions.

Type unions

Type unions are just a special abstract type consisting of the union of other types. For example we can define a union of integers and strings:

IntOrString = Union{Int,AbstractString}
isa(1, IntOrString)
isa("a", IntOrString)

In Julia, Void (renamed Nothing in Julia ≥0.7) is a first class type. So we can define a type union consisting of String and Void. We might call this OptionalString.

OptionalString = Union{String, Void}
isa("hello", OptionalString)
isa(nothing, OptionalString)

We can extend this to any type by creating a parametric version called Optional.

Optional{T} = Union{T, Void}
isa("hello", Optional{String})
isa(nothing, Optional{String})

The Optional type takes a parameter, T, that defines a type union between T and Void.

Composite type with Optional values

Using the Optional parametric composite type, we can redefine Foo to accept either strings or nothing:

struct Foo
    bar::Optional{String}
    qux::Optional{String}
end
Foo("hello", nothing)

The constructor method from above can be used without modification.

mydict = Dict("bar" => "hello")
Foo(mydict)

So now we have a constructor method that converts a Dict to our composite type that works when the Dict contains extraneous keys or is missing keys! Just note that if every field in the composite type is “optional” then you may get an instance where every value is nothing (i.e. none of the Dict keys match the type field names).

Bringing it back to the real world

In the original example that motivated this post, I was converting a Dict containing the Dark Sky API response to a composite type. We saw how to do this with a constructor method, but this only worked when the Dict contained every field name of the composite type. Then we saw how to use the Optional type union to allow fields to be undefined.

The DarkSky.jl code is fairly similar to the trivial example above:

# Define "Optional" parametric type
Optional{T} = Union{T, Void}

# Define the DarkSkyResponse composite type
struct DarkSkyResponse
    latitude::Float64
    longitude::Float64
    timezone::String
    currently::Optional{Dict}
    minutely::Optional{Dict}
    hourly::Optional{Dict}
    daily::Optional{Dict}
    alerts::Optional{Dict}
    flags::Optional{Dict}
end

# Define the constructor method
function DarkSkyResponse(x::Dict)
    field_names = String.(fieldnames(DarkSkyResponse))
    DarkSkyResponse((get.(x, field_names, nothing))...)
end

This code flexibly handles cases where the Dark Sky API doesn’t return one or more of the fields, such as the case when the user specifically requests only a subset of fields.