I googled "phantom types nim-lang" and nothing much came up, so I put this together...
nim
type
Validated = distinct void
AwaitingValidation = distinct void
Address*[T] = distinct string
proc validate[T](addressCandidate: Address[T]): Address[Validated] =
# do some serious validation here
return Address[Validated](addressCandidate)
proc sendTo(validatedAddress: Address[Validated]): bool =
# prepare some message and send it
return true
let ac = Address[AwaitingValidation]("possibly an address")
let validated = validate(ac)
discard sendTo(validated)
discard sendTo(ac) # type error
I don't get to use Nim as often as I would like, so if there's a nicer way to present it, please let me know. for a slightly different approach using a statically constained generic and an enum, here is a snippet that I had in some old notes (came up when watching a phantom types in gleam talk at fosdem 23, I recall it was @pmunch who gave me the good hint on how to make them this way)
import std / options
type ButtonKind = enum NoIcon, WithIcon
type Button[T: static ButtonKind] = object
label: string
icon: Option[string]
func initButton(label: string): Button[NoIcon] =
result.label = label
func withIcon(button: Button[NoIcon], icon: string): Button[WithIcon] =
result.label = button.label # do I need to do that? converting as:
# result = button.Button[WithIcon] # does not work...
result.icon = some(icon)
echo initButton("Hi").withIcon("👋")
assert not compiles(initButton("Hi").withIcon("👋").withIcon("❌"))
There isn't much to it, most of the time you're better off with:
type
Foo = object
first, last: string
street: string
proc parseFoo(x: string): Foo
I’m not sure how practical this is, but I think Nim’s compile-time execution is so cool.
import std / options
import std / strutils
type ButtonStateEnum = enum WithLabel, WithIcon
type ButtonState = set[ButtonStateEnum]
type Button[T: static ButtonState] = object
label: string
icon: Option[string]
func initButton(): Button[default(ButtonState)] =
result
func withLabel[T: static ButtonState](button: Button[T], label: string): Button[T + {WithLabel}] =
result = cast[typeof(result)](button)
result.label = label
func toUpper[T: static ButtonState](button: Button[T]): Button[T] =
when WithLabel notin button.T: {.error: "Button.label is not set.".}
result = cast[typeof(result)](button)
result.label = result.label.toUpper()
func withIcon[T: static ButtonState](button: Button[T], icon: string): Button[T + {WithIcon}] =
when WithIcon in button.T: {.error: "Button.icon is already set.".}
result = cast[typeof(result)](button)
result.icon = some(icon)
echo initButton().withLabel("hello").toUpper().withIcon("👋")
# echo initButton().toUpper() # <-- error: Button.label is empty.
# echo initButton().withIcon("👋").withIcon("👋") # <-- error: Button.icon is alreay set.
Something like this would probably be easier to read. It handles all the possible states in a very straightforward manner. It won't catch address formatting errors at compile time, though, if that's what you're really looking for.
type
PossibleAddress = distinct string
ValidAddress = distinct string
InvalidAddress = distinct string
proc isValidAddress(address: PossibleAddress): bool =
return address.string == "valid address"
proc sendMessage(address: ValidAddress): bool =
return true
proc sendMessage(address: InvalidAddress): bool =
return false
proc sendMessage(address: PossibleAddress): bool =
if isValidAddress(address):
result = sendMessage(ValidAddress(address))
else:
result = sendMessage(InvalidAddress(address))
let goodMessage = "valid address".PossibleAddress
let badMessage = "invalid address".PossibleAddress
let validMessage = "valid address".ValidAddress
let invalidMessage = "invalid address".InvalidAddress
echo sendMessage(goodMessage)
echo sendMessage(badMessage)
echo sendMessage(validMessage)
echo sendMessage(invalidMessage)