Happy New Year!
I'm posting this here as it's not refined enough for an RFC, yet. Feedback appreciated.
In this proposal it is a simple extension to the existing enum construct:
type
Option[T] = enum
of None: discard
of Some: T
Either[A, B] = enum
of Le: A
of Ri: T
BinaryNode = object
a, b: ref Node
UnaryNode = object
a: ref Node
Node = enum
of BinaryOpr: BinaryNode
of UnaryOpr: UnaryNode
of Variable: string
of Value: int
The new form of an enum that uses an of syntax is called the "sum enum". Constructing an enum branch uses the branch name plus its payload in parenthesis. However, BinaryOpr(BinaryNode(a: x, b: y)) can be shortened to BinaryOpr(a: x, b: y), an analogous shortcut exists for tuples.
To access the attached values, pattern matching must be used. This enforces correct access at compile-time.
The syntax of Branch as x can be used to unpack the sum type to x.
proc traverse(n: ref Node) =
case n[]
of BinaryOpr as x:
traverse x.a
traverse x.b
of UnaryOpr as x:
traverse x.a
of Variable as name:
echo name
of Value as x:
counter += x
of Branch as variable is sugar for of Branch(let variable). of Branch(var variable) is also available allowing mutations to variable to write through to the underlying enum object.
The syntax of Branch as x can later be naturally extended to if statements: if n of BinaryOpr as x or if n of Some(var n).
Proposed syntax:
case n
of BinaryOpr(var a, UnaryOpr(let b)) if a == b:
a = ... # can write-through
There are two new macros that can traverse enums:
For example:
type
BinaryNode = object
a, b: ref Node
UnaryNode = object
a: ref Node
Node = enum
of BinaryOpr: BinaryNode
of UnaryOpr: UnaryNode
of Variable: string
of Value: int
proc store(f: var File; x: int) = f.write x # atom
proc store(f: var File; r: ref Node) = store r[] # deref `ref`
proc store[T: object](f: var File; x: T) =
# known Nim pattern:
for y in fields(x): store y
proc store[T: enum](f: var File; x: T) =
unpack x, f.store, f.store
# `unpack` is expanded into a case statement:
# `case x[]
# of Val as val: f.store(kind); f.store(val)`
# ...
proc load[T: enum](f: var File; x: typedesc[T]): T =
let tmp = f.load[:IntegralType(T)]()
result = constructEnum(T, tmp, f.load)
# constructEnum is expanded into a case statement:
# `case tmp
# of Val: Val(f.load[:BranchType]())`
# ...
This is great! Looking forward to having this in the language.
Any chance we can have some sugar to avoid declaring a payload object for branches? i.e. the following would implicitly declare anonymous types for BinaryOpr and UnaryOpr:
type
Node = enum
of BinaryOpr: (a, b: ref Node)
of UnaryOpr: (a: ref Node)
of Variable: string
of Value: int
I would agree on the enum front, not based on any of the points beef made but more fundamentally:
The way this looks to me, the naive and unknowing user, is that it's a different of writing object variants where you directly write the enum into the object variant.
However what I'm mildly stuck on is how you those "enums-in-variants" interact with stuff like e.g. parseEnum
Good gotcha, reusing the enum typeclass for sum enums will be too confusing. Back to case?
type
Option[T] = case
of None: discard
of Some: T
Either[A, B] = case
of Le: A
of Ri: T
BinaryNode = object
a, b: ref Node
UnaryNode = object
a: ref Node
Node = case
of BinaryOpr: BinaryNode
of UnaryOpr: UnaryNode
of Variable: string
of Value: int
I'm also thinking about tuples. Are they supported in sum types?
type
Device = case
of Mobile: (string, int)
of Server: (string, int, bool)
let device: Mobile = ("iPhone", 15)
How will they be unpacked? Can I do this?
case device:
of Mobile(name, model):
of Server(name, model, online):
OR do I need to do this?
case device:
of Mobile((name, model)):
of Server((name, model, online)):
Regarding your questions, this is how I see them:
case x:
of StrValue(s): # if s is of type string
of Minus(m): # then m is of type UnaryOp
of IfStmt(ifs): # and ifs is of type IfTree
Should this even be allowed:
of IfStmt(cond, thenPart, elsePart):
Wouldn't this be clearer:
of IfStmt((cond, thenPart, elsePart)):
of IfStmt((cond, thenPart, elsePart) as ift):
doSomething(cond)
doSomething(ift)
There's also this:
of IfStmt(IfTree(cond, thenPart, elsePart)):
Or this:
of IfStmt(IfTree(cond, thenPart, elsePart) as ift):
doSomething(cond)
doSomething(ift)
Yes tuples are allowed and there is no need for double (()) as it's ugly.
How about this syntax:
case mytree
of StrValue as s: # if s is of type string
of Minus as m: # then m is of type UnaryOp
# or
of Minus as (m): # unpacking, m is of type Tree
of IfStmt as (cond, le, ri): # unpacking
I think that's better, since for both single and multi-field objects, the (...) is consistently applied to perform the unpacking.
Perhaps it could even support multiple unpacking variants.
For illustrative and comparison purposes:
of IfStmt as (cond, left, right):
of IfStmt as (_, _, right):
of IfStmt as (cond, ...):
of IfStmt as _:
of IfStmt:
Or something along these lines.
I replied on the GitHub thread, cross posting here.
https://github.com/nim-lang/RFCs/issues/548
To me, this proposal is an improvement over the current object variant mechanism. I really like
case foo of bar as baz
being distinct from
case foo of bar(var baz1, let baz2)
I feel both forms have real value.
I'm still not really sure why the discrimination isn't simply done on the inner type, without adding a new name to each type in the sum. After all, it's really the type we're interested in. Even an "anonymous" type could be be used with this. It would also make the 'as' simpler, although perhaps less useful. But maybe that's a good thing?
type
BinaryNode = object
a, b: ref Node
UnaryNode = object
a: ref Node
Node = case
of BinaryNode
of UnaryNode
of OtherNode = ref Node # "anonymous" type
of string
of Name = string # "anonymous" type
of int
proc traverse(n: ref Node) =
case n[] as x # The "as x" doesn't seem as helpful this way, but it's still nice.
of BinaryNode:
traverse x.a
traverse x.b
of UnaryNode:
traverse x.a
of OtherNode:
traverse x
of string:
echo x
of Name:
echo "Hello ", x
of int:
counter += x
var myNode: Node[int] = 42
# Which is better looking? Which is easier to implement?
if myNode[int]:
echo "The answer is: ", myNode + 0
elif myNode of Name:
echo "Hello, ", myNode, "!"
elif myNode of string as ovaltine:
echo "The secret decoder message of the day is: ", ovaltine
I'm still not really sure why the discrimination isn't simply done on the inner type, without adding a new name to each type in the sum. After all, it's really the type we're interested in.
Since then I came to the same conclusion and so the as syntax is not needed at all. Also I'm now in favor of case x of Some(a, b) pattern matching where a and b are mutable or immutable depending on x. This way no syntax extension is required at all for the case statement and pattern matching is more concise.