Proposal: macro for unit or literal type

I am modelling widths for table cells. Widths can be an number or the special value “auto”. A bit like css.

For the value “auto” I can use a string but it’s too board.

What I want is a unit or literal type. In Crystal I don’t actually need anything special. I can do this:

class Auto
end

alias Width = Int32 | Auto
cell(width: 5)
cell(width: Auto.new)

This is ok but structs and a singleton is probably better

struct Auto
  INSTANCE = self.allocate
  def self.new
    INSTANCE
  end
end
cell(width: Auto.new) # is now a singleton?

or maybe a enum of one, just a bit annoying to have the double name

enum Auto
  Auto
end

alias Width = Int32 | Auto
cell(width: :auto) # auto casting is nice

What I don’t like is the verbosity. Like record is a built in macro I wondering what people think about adding a macro for unit types and the performance implication for modelling things like this?

macro unit(*names)
  {% for name in names %}
    enum {{name.id}}
      {{name.id}}
    end
  {% end %}
end

unit Auto, None

alias Width = Int32 | Auto | None # type safe!

I’ve read other threads on suggesting adding literal types like in typescript. This is not that.

To be honest I’m not sure it’s a common enough usecase for stdlib.

Seems like an easy enough thing to carry around in your utils.cr if you end up using it a lot though :)

That’s fair. To be honest I am not sure it’s common enough as well :)

I saw at least a few threads complaining about enums and symbols and literal types. So I wonder how many people have similar pain points.

For single values, you can also use a self-extending module. It doesn’t support autocausting, though.

module Auto
  extends self
end

alias Width = Int32 | Auto

def cell(width : Width)
end

cell(Auto)

If you have multiple values, it’s probably better to combine them into a single enum:

enum WidthValue
  Auto
  None
end

alias Width = Int32 | WidthValue

Yeah I tried the self extending module already and if it worked it would be a pretty good option because cell(Auto) is quiet nice as a public API

Unfortunately modules as a constant are of type class so it’s not the same thing

module Auto
   extends self
end

pp Auto.class # class

Enum Auto
  Auto
end

pp Auto.class # Auto

Why do you need the type to be not class?

Ok this is interesting. Both Auto.class and typeof(Auto) returns Class. But when using is_a? it is possible to work out that it’s Auto:Module.

This is good to know because I thought it won’t be able to differentiate between multiple modules when using case

Is there a something like typeof that I can call on the module to show the information Auto:Module instead of just Class?

class and typeof return class because Auto extends itself but that doesn’t show up as a direct ancestor.

You don’t need that thouch. You can just directly compare the value. For example:

case width
when Auto
  # it's Auto
when Int32
  # it's number
end