Wrapping Unitful macros

Inspired by a commenter on Unitful.jl I’m trying to do unitful calculations with bookshelves: how many books do I have in 10 book cases?

(20book / shelf) * (8shelf / case) * (10case) == 1600book

I would like to replace the complicated invocations of @dimension and @refunit with a simple @countable book as suggested in the issue, but I’m having trouble with the macro.

using Unitful


@dimension BookDimension "BookDimension" BookDim
@refunit book "book" Book BookDimension false

@dimension ShelfDimension "ShelfDimension" ShelfDim
@refunit shelf "shelf" Shelf ShelfDimension false

@dimension CaseDimension "CaseDimension" CaseDim
@refunit case "case" Case CaseDimension false


@assert (20book / shelf) * (8shelf / case) * (10case) == 1600book


macro countable(name)
    quote
        Unitful.@dimension $(name)Dimension "$(name)Dimension" $(name)Dim
        Unitful.@refunit $name "$name" $name $(name)Dimension false
    end
end

@countable book
@countable shelf
@countable case
@countable book
ERROR: LoadError: MethodError: no method matching var"@dimension"(::LineNumberNode, ::Module, ::Symbol, ::Symbol, ::Expr, ::Symbol, ::Symbol)
1 Like

For good reasons, macros do not work by substituting strings, but instead represent code as a data structure, i.e., if you want to insert new names in the expansion you have to create them first:

macro countable(name)
    # name is the symbol :book when calling `@countable book`
    namestr = uppercasefirst(string(name))  # This is now the string "Book"
    dimsym = Symbol(namestr, "Dim")  # From that, we create the symbol `:BookDim`
    dimstr = namestr * "Dimension"  # and the string "BookDimension"
    # Using this new symbol and string, the desired expression can be constructed
    esc(quote
        Unitful.@dimension $(Symbol(namestr, "Dimension")) $dimstr $dimsym
        # Insert your second expression here ...
    end)
end

Note that escaping the whole expansions is considered bad practice as it breaks macro hygiene. Here, it seems to be required as @dimension in turn introduces additional symbols for type parameters which are incorrectly namespaced otherwise.

julia> @macroexpand1 @countable book
quote
    #= REPL[2]:8 =#
    #= REPL[2]:8 =# Unitful.@dimension BookDimension "BookDimension" BookDim
end

julia> @countable book
BookDimension
3 Likes

Filling out the demo

using Unitful
macro countable(name)
    namestr = uppercasefirst(string(name))
    dimsym = Symbol(namestr, "Dim")
    dimstr = namestr * "Dimension"
    esc(quote
        Unitful.@dimension $(Symbol(namestr, "Dimension")) $dimstr $dimsym
        Unitful.@refunit $name $(string(name)) $(Symbol(namestr)) $(Symbol(dimstr)) false
    end)
end

@countable book
@countable shelf
@countable case

@assert (20book / shelf) * (8shelf / case) * (10case) == 1600book