pietroppeter / rc23winter1

main repo for public activities during recurse center (2023 Winter 1 batch)
MIT License
0 stars 0 forks source link

Phantom types #1

Open PMunch opened 9 months ago

PMunch commented 9 months ago

Had a look at your phantom types test. Here is a small fix for the generic casting, as well as an attempt to answer the "what happens if you have multiple phantom types" question.

# phantmom types in Nim
# inspired by Hayleigh's talk about Phantom types in Gleam
# the key ingredient for Nim is constraining the generic to a static type
# (thanks PMunch)
import std / options

type
  IconState = enum NoIcon, WithIcon
  BorderState = enum NoBorder, WithBorder

type Button[I: static IconState, B: static BorderState] = object
  label: string
  icon: Option[string]
  border: Option[string]

func initButton(label: string): Button[NoIcon, NoBorder] =
  result.label = label

func withIcon[B](button: Button[NoIcon, B], icon: string): Button[WithIcon, B] =
  result = button.Button[:WithIcon, B]
  result.icon = some(icon)

func withBorder[I](button: Button[I, NoBorder], color: string): Button[I, WithBorder] =
  result = button.Button[:I, WithBorder]
  result.border = some(color)

echo initButton("Hi").withIcon("πŸ‘‹")
echo initButton("Hi").withIcon("πŸ‘‹").withBorder("red")

assert not compiles(initButton("Hi").withIcon("πŸ‘‹").withIcon("❌"))
assert not compiles(initButton("Hi").withIcon("πŸ‘‹").withBorder("red").withBorder("blue"))
pietroppeter commented 9 months ago

Ooh looks nice! It seems to be calling for meta programming :)

PMunch commented 9 months ago

Definitely something which could be tidied up a bit by some nice metaprogramming, yes!

Played around with this concept a bit more, it's a bit less general, and a bit more hacky, but even more type-safe. With this you can't even unpack the value from those fields which don't have a value:

import std / options

type
  StaticOpt[C: static bool, T] = object
    when C:
      data: T
    else:
      _: array[sizeof(T), byte] # Add this so that the object is always the same size

proc `$`[C: static bool, T](s: StaticOpt[C, T]): string =
  when C: "some(" & $s.data & ")"
  else: "none(" & $typeof(T) & ")"

proc get[C: static bool, T](s: StaticOpt[C, T]): T =
  when C: s.data
  else: {.error: "Data not available".}

type Button[IconState, BorderState: static bool] = object
  label: string
  icon: StaticOpt[IconState, string]
  border: StaticOpt[BorderState, string]

func initButton(label: string): Button[false, false] =
  result.label = label

func withIcon[B](button: Button[false, B], icon: string): Button[true, B] =
  result = cast[Button[true, B]](button)
  result.icon.data = icon

func withBorder[I](button: Button[I, false], color: string): Button[I, true] =
  result = cast[Button[I, true]](button)
  result.border.data = color

echo initButton("Hi").withIcon("πŸ‘‹")
echo initButton("Hi").withBorder("blue").withIcon("πŸ‘‹")
echo initButton("Hi").withIcon("πŸ‘‹").withBorder("red")
echo initButton("Hi").withIcon("πŸ‘‹").icon
echo initButton("Hi").icon
echo initButton("Hi").withIcon("πŸ‘‹").icon.get

assert not compiles(initButton("Hi").icon.get)
assert not compiles(initButton("Hi").withIcon("πŸ‘‹").withIcon("❌"))
assert not compiles(initButton("Hi").withIcon("πŸ‘‹").withBorder("red").withBorder("blue"))