-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Sum types, 2024 variant #548
Comments
Some notes with regards to the implementation (feel free to ignore if not interested): This design is about as simple as it gets for the parser (parse
type
Option[T] = case
of None: discard
of Some: T
List[T] = case
of None: discard
of Cons: (T, ref List[T])
let x: Option[int] = None() should work. |
The construction syntax I'm interested in to get comparable ergonomics of use with other languages (for let x: Either[int, string] = Le(42) ie |
Given that |
Not applicable anymore as there is an inherent difference between |
The devel compiler has |
I think this will be better for pattern matching case n
of BinaryOpr(a: var le, b: UnaryOpr(a: let ri)) if le == ri:
le = ... # can write-through It's more like an object constructor case n
of BinaryOpr(b: UnaryOpr(a: let ri)):
echo ri # I dont care about left operand in BinaryOpr |
@ASVIEST "don't cares" should be written as |
this may be inconvenient for objects with a fairly large number of fields. Maybe we can allow both of these syntaxes like #517 (or maybe better #418) ? type
SampleChunk = object
chunkID: uint32
size: uint32
manufacturer: uint32
product: uint32
samplePeriod: uint32
unityNote: uint32
pitchFraction: uint32
smpteFmt: uint32
smpteOffset: uint32
loopCnt: uint32
dataSize: uint32
sampleLoops: seq[SampleLoop]
data: seq[byte]
SampleLoop = object
id: uint32
typ: uint32
start: uint32
endBlock: uint32
frac: uint32
playbacks: uint32
WaveChunk = case
of Sample: SampleChunk
...
var x = Sample()
case x
of Sample(_, _, _, _, _, _, _, _, _, _, let dataSize, _, _):
echo dataSize
else: discard
# vs
case x
of Sample(dataSize: let dataSize):
echo dataSize
else: discard allowing both syntaxes it will be convenient both for objects with a large number of fields and for objects with a small number |
It's just: case x
of Sample(let x):
echo x.dataSize
else: discard
To select a single field there is no reason to unpack stuff. Keep things simple. |
It means that in case n
of BinaryOpr(var a, UnaryOpr(let b)) if a == b:
a = ... # can write-through let b is UnaryOpr and not ref Node ? |
Maybe you mean this ? case x
of Sample():
echo x.dataSize
else: discard |
Sorry, I made a mistake. Please re-read the proposal. :-/ |
I mean this: case x
of Sample as y:
echo y.dataSize
else: discard
|
Positional pattern matching should only be allowed for tuple and enumeration types, as these types already have prominent features that rely on the ordering of their fields. In contrast, object types do not1. Allowing positional pattern matching for object types would result in their public field ordering forming part of their API; developers would not be able to move fields around, nor insert new fields before existing fields, nor remove existing fields not at the end of the object, without introducing compatibility-breaking changes. Considering that only a small subset of objects have a conceptually "natural" field ordering, introducing such behavior doesn't make sense. For objects that do have a conceptually significant field ordering, a tuple type is more appropriate. Footnotes
|
In general, I agree, but I think that instead of the complete absence of a positional pattern matching for objects, it should be left for objects with a new pragma |
Can we focus on the question of whether |
They could be merged into (assuming of Branch as BranchType(let x):
of Branch as (let x): # for anonymous objects/tuples, maybe requires trailing comma
of Branch as (let x, let y):
of Branch as let (x, y):
of Branch as (fieldName: let x):
# if we don't care to make bindings explicit, we can always assume identifiers to be `let`/`var`
of Branch as BranchType(x):
of Branch as (x):
of Branch as (x, y):
of Branch as var x: # makes this possible for what it's worth
of Branch as let x: # and this
# this would be a reuse of a potential mechanism like:
BranchType(let x) = y
let BranchType(x) = y # x assumed to be let
let (x) = y # language already allows this to unpack unary tuples
(let x, let y) = z If all of these are ugly, I don't see how |
I don't understand your idea. |
Personally, I would prefer something closer to Rust: type
Option[T] = case
of None()
of Some(T)
Either[A, B] = case
of Le(A)
of Ri(T)
Node = case
of BinaryOpr:
a, b: ref Node
of UnaryOpr:
a: ref Node
of Variable(string)
of Value(int) At least for the tuple variants, how the definition looks would match how the destructuring looks. |
I think that when sum types is mapping type
Node = case
of BinaryOpr: (a, b: ref Node)
of UnaryOpr: (a: ref Node)
of Variable(string)
of Value(int) doing it like a Rust removes flexibility without increasing the simplicity of the code. It is also possible that such sum types are simpler to implement in backends. It also fits better into the logic of the case statement: type
Sth = case
of A1: B1
of A2: B2 B1 is not a just field list, it's type |
Regarding testing the underlying type of a sum-typed variable, requiring use of a proc traverse(n: ref Node) =
case kind(n[])
of BinaryOpr:
... It is also consistent with how most sum types are currently implemented (via object variants): type Node = object
case kind: NodeKind
of nkBinaryOpr:
...
proc traverse(n: ref Node) =
case n[].kind
of nkBinaryOpr:
... Furthermore, it would allow retrieving the "kind" of a sum-typed variable in other contexts (such as logging/debugging). proc traverse(n: ref Node) =
echo repr(kind(n)) Regarding using sum types, couldn't the current rules for object variants be used, where a variable can be used as a tested type when the compiler can statically determine that it is that actual type? proc traverse(n: ref Node) =
case kind(n[])
of BinaryOpr:
echo(n.left, n.right) # BinaryOpr-specific fields
of UnaryOpr:
echo(n.term) # UnaryOpr-specific field
... Again, this would be consistent with how object variants are currently handled. I would need to defer to the compiler-devs, but I believe this might also allow re-use of existing compiler code too. I know that the above syntax isn't "exciting" or "new", but it is consistent, reducing the amount of surprise/complexity that is needed to use sum objects - from a user's perspective, a sum type works "just like" an object variant (which it arguably is). It also makes migrating existing sum-types-via-object-variant code very easy. Pattern matching, I feel, would be best implemented in a different proposal. I think that tuple unpacking and templates already serve to address most of the "boilerplate" that pattern matching is usually meant to address. (If desired, I can go into more detail regarding what I feel are the benefits of the above syntax, but I figured I would write a shorter explanation first) |
Didn't see anyone else bringing this up but it is a good point that passing around the kind of the type as a value should still be possible. A less ambiguous name for this could be type Foo = case
of A: int
of B: string
let foo = B("abc")
let kind = caseof(foo)
echo kind # B Then we could also have |
It feels like the latter is "unpacking" the former is "assignment". I lean towards those two having different meanings. It is also nice to NOT have two syntaxes to do the same thing. Outside of pattern matching. Branch(x) is a constructor - x is the internal value
I noticed it right away. |
It's an improvement over the current object variant mechanism. I like the 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?
|
Sum types, 2024 variant
There is a new type construct,
case
that gives Nim sum types, comparable to ML's, Rust's, etc.Constructing a case 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.
Access via
as
The syntax
of Branch as x
can be used to unpack the sum type tox
.of Branch as variable
is the basic form. Forof Branch: T
thevariable
has the typeT
orvar T
depending on the mutability of the expressionx
incase x
.Pattern matching
A variable of type
T
might be inconvienent so there also pattern matching combined with unpacking:These new syntaxes
of Branch as x
andof Branch(let x)
can later be naturally extended to if statements:if n of BinaryOpr as x
orif n of Some(var n)
.More complex pattern matching
Proposed syntax:
Serialization
There are two new macros that can traverse sum types:
constructCase
takes in a typeT
and an expression in order to construct a case typeT
.unpackCase
takes in a value of a case type and an expression in order to traverse the data structure.For example:
Anon object types
Later we can add more sugar so that the definition can be simplified to:
This way the simplicity is kept that every branch is tied to exactly one type which makes iteration over
case
types in a generic context much easier.The text was updated successfully, but these errors were encountered: