Object variants

Often an object hierarchy is overkill in certain situations where simple variant types are needed. Object variants are tagged unions discriminated via a enumerated type used for runtime type flexibility, mirroring the concepts of sum types and algebraic data types (ADTs) as found in other languages.

An example:

  1. # This is an example how an abstract syntax tree could be modelled in Nim
  2. type
  3. NodeKind = enum # the different node types
  4. nkInt, # a leaf with an integer value
  5. nkFloat, # a leaf with a float value
  6. nkString, # a leaf with a string value
  7. nkAdd, # an addition
  8. nkSub, # a subtraction
  9. nkIf # an if statement
  10. Node = ref NodeObj
  11. NodeObj = object
  12. case kind: NodeKind # the ``kind`` field is the discriminator
  13. of nkInt: intVal: int
  14. of nkFloat: floatVal: float
  15. of nkString: strVal: string
  16. of nkAdd, nkSub:
  17. leftOp, rightOp: Node
  18. of nkIf:
  19. condition, thenPart, elsePart: Node
  20. # create a new case object:
  21. var n = Node(kind: nkIf, condition: nil)
  22. # accessing n.thenPart is valid because the ``nkIf`` branch is active:
  23. n.thenPart = Node(kind: nkFloat, floatVal: 2.0)
  24. # the following statement raises an `FieldDefect` exception, because
  25. # n.kind's value does not fit and the ``nkString`` branch is not active:
  26. n.strVal = ""
  27. # invalid: would change the active object branch:
  28. n.kind = nkInt
  29. var x = Node(kind: nkAdd, leftOp: Node(kind: nkInt, intVal: 4),
  30. rightOp: Node(kind: nkInt, intVal: 2))
  31. # valid: does not change the active object branch:
  32. x.kind = nkSub

As can been seen from the example, an advantage to an object hierarchy is that no casting between different object types is needed. Yet, access to invalid object fields raises an exception.

The syntax of case in an object declaration follows closely the syntax of the case statement: The branches in a case section may be indented too.

In the example the kind field is called the discriminator: For safety its address cannot be taken and assignments to it are restricted: The new value must not lead to a change of the active object branch. Also, when the fields of a particular branch are specified during object construction, the corresponding discriminator value must be specified as a constant expression.

Instead of changing the active object branch, replace the old object in memory with a new one completely:

  1. var x = Node(kind: nkAdd, leftOp: Node(kind: nkInt, intVal: 4),
  2. rightOp: Node(kind: nkInt, intVal: 2))
  3. # change the node's contents:
  4. x[] = NodeObj(kind: nkString, strVal: "abc")

Starting with version 0.20 system.reset cannot be used anymore to support object branch changes as this never was completely memory safe.

As a special rule, the discriminator kind can also be bounded using a case statement. If possible values of the discriminator variable in a case statement branch are a subset of discriminator values for the selected object branch, the initialization is considered valid. This analysis only works for immutable discriminators of an ordinal type and disregards elif branches. For discriminator values with a range type, the compiler checks if the entire range of possible values for the discriminator value is valid for the chosen object branch.

A small example:

  1. let unknownKind = nkSub
  2. # invalid: unsafe initialization because the kind field is not statically known:
  3. var y = Node(kind: unknownKind, strVal: "y")
  4. var z = Node()
  5. case unknownKind
  6. of nkAdd, nkSub:
  7. # valid: possible values of this branch are a subset of nkAdd/nkSub object branch:
  8. z = Node(kind: unknownKind, leftOp: Node(), rightOp: Node())
  9. else:
  10. echo "ignoring: ", unknownKind
  11. # also valid, since unknownKindBounded can only contain the values nkAdd or nkSub
  12. let unknownKindBounded = range[nkAdd..nkSub](unknownKind)
  13. z = Node(kind: unknownKindBounded, leftOp: Node(), rightOp: Node())