A Slight Delight - Compile-time checking things

Underjord is a tiny, wholesome team doing Elixir consulting and contract work. If you like the writing you should really try the code. See our services for more information.

This was a short-but-sweet thing that struck me while working with a client code-base. It was trivial but both useful and delightful and it is a type of thing I haven't been able to do in Python, PHP and Javascript in quite the same way.

Initially, the code was something like this:

elixir
defmodule MyApp.Schemas do 
    alias ExJson.Schema

    def my_schema do 
        %{
            "properties" => %{
                "email" => %{
                    "type": "string", 
                },
                "password" => %{
                    "type": "string", 
                }
            }
        } |> Schema.resolve()
    end
end

So for this JSON schema library the Schema.resolve function verifies that your JSON schema is defined in a valid manner and lives up to the JSON schema specification. So it doesn't do anything related to user-provided data or parsing JSON data. While familiarizing myself with the library I saw that you should probably avoid calling resolve repeatedly. Which makes sense, if the schema doesn't change, why call it again. Some previous developer probably missed that. It happens.

What I ended up doing was:

elixir
    defmodule MyApp.Schemas do 
        alias ExJson.Schema
    
        @my_schema Schema.resolve(%{
            "properties" => %{
                "email" => %{
                    "type": "string", 
                },
                "password" => %{
                    "type": "string", 
                }
            }
        })

        def my_schema do 
            @my_schema
        end
    end

So what does this give us? Well, module attributes are resolved at compile-time. This is why @my_secret System.get_env("MY_SECRET") often leads to unwanted behavior. But in this case, our schemas are part of our code and will not change at run-time or between environments. So we can shift the resolution to happen at compile-time and never have to do it at run-time.

  • The cost of calling Schema.resolve is paid up front at compile-time.
  • Errors in defining JSON schemas show up at compile-time rather than run-time when you attempt to validate something.

Having put much less time into compiled languages this was delightful. Being able to shift some checks back to compile-time is incredibly useful. I've also had a reason to write some macros which was fun.

If you have some suggestions, corrections or thoughts on this let me know at lars@underjord.io.

Underjord is a 4 people team doing Elixir consulting and contract work. If you like the writing you should really try the code. See our services for more information.

Note: Or try the videos on the YouTube channel.