neko: ne thing that I've thought about in the past is mixing records and enums into some sort of hybrid type that has its variant (like an enum) and also fields regardless of the variant. I've also thought about allowing enum variants to be their own type, so you can e.g. have a function return IPAddress.IPv4.
icefox: I saw a lang recently that actually kinda did this, what was it... like if you did
enum Foo {
A { a: i32, y: String },
B { a: i32 },
C { a: i32, something_else: Thing, whatever: Whatever }
}
you could write
let x: Foo = make_some_foo(); x.a
and it would access the a field on whatever variant it was.Hmm, what if you wrote it like this:
enum Foo {
val a: i32;
A { y: String },
B { },
C { something_else: Thing, whatever: Whatever }
}
and then you could also tell it what and were to put the discriminant and what type to use for it, just by making it a field. shit this is actually a good idea
neko: what syntax would you use to make a field the discriminator complex discriminators that are derived from more than one field yes or no? (probably better to say no due to complexity)
icefox: no (not yet)
I think one thing to note here could be how to handle fields shared between some variants only. I vaguely recall I felt a need for something like this more than once in my life/career; though also not daily/monthly I think (but maybe that could happen more often if they were available more easily?..).
Now that I think of it, I guess it honestly makes me think of this being a "representation of states/nodes/vertices in a state machine". And data being sometimes shared/retained through a few states of the state machine; notably, also such spans could be overlapping. Say:
enum States { A { foo: Foo }, B { foo: Foo, bar: Bar, zing: Zing }, C { bar: Bar } D { foo: Foo, zing: Zing } }
thus for example representing a state machine like below:
(edit: hm, some random ideas for syntaxes for the above:
enum State { val foo: Foo in A, B, D; val bar: Bar in B, D; val zing: Zing in B, D; A { } B { } C { } D { } } ## ok, I hate the one above... enum States { A { foo: Foo } B { A.foo, bar: Bar, zing: Zing }, C { B.bar }, D { B.foo, B.zing } # could be allowed as well as: D { A.foo, B.zing } ? } ## hm, the one above seems like it could actually make some sense... 🤔
)
Another thought coming to mind, whether field names could be then still freely shared between variants if they needed to have different types, e.g.:
enum AST { AddInts { left: int, right: int }, AddStrings { left: String, right: String }, } enum States2 { A { foo: Foo }, B { foo: FooSlightlyModified }, } enum HTMLBlock { Div { children: []HTMLBlock } P { children: []HTMLSpan } }
I think one thing to note here could be how to handle fields shared between some variants only...
Yeah, in my brain this basically becomes a subtyping problem, and then I stick a "not worth the effort of solving" label on it and put it on a shelf. :-P In Rust-y "never let someone dodge errors just 'cause it's tedious" style there's only really two options: you know this pattern match will never fail, so you don't actually need the pattern match, or you know this pattern match might fail sometimes, so they always have to do the whole thing. Declaring the data is a whole lot less annoying to me than having to pattern match on it every single time, like so:
match thing { A { foo } => ..., B { foo, bar, zing } => ..., C { foo, bar } => ..., D { foo, zing } => ..., }especially when what I really want to do is just do something to
foo
. All I want to write isdo_stuff(thing.foo)
. I can't really think of any sensible way to make it so you can writedo_stuff(thing.bar)
. If you insist that it only works when all fields have the same type though, perhaps you can write:match thing { _ { bar } => do_stuff(bar), _ => ..., }and it would match on any variant that had a field
bar
. Again, assuming that they all have the same type.(Also I appreciate the hell out of you making a heckin' diagram about this. :D)
That said, there's plenty room for clever tricks. Like in Crystal if you have its equivalent of an
Option
type, you can write:x : String? = something() if x do_stuff_with(x) endOutside the
if
block,x
has typeString | Nil
and inside theif
block,x
has typeString
. So it's literally justlet x: Option<String> = something(); if let Some(x2) = x { do_stuff_with(x2); }but with like 90% less ceremony to it. I dunno that I actually want to do something like that in Garnet --Rust does a very good job of making questionable things a bit inconvenient to write so that you have some time to think about how much you really need them-- but I appreciate how slick it is.
I dunno that I actually want to do something like that in Garnet --Rust does a very good job of making questionable things a bit inconvenient to write so that you have some time to think about how much you really need them-- but I appreciate how slick it is.
I think the point and argument of Garnet is in part that Rust makes some things a bit too much inconvenient, no? 😜 I always liked the phrase, sometimes used for various technologies, that: "it feels like there's a smaller, simpler [language] hiding inside [Rust] and wanting to escape" - I'm kind of assuming that's what drives you forward, and it's at least what I'm hoping for Garnet to try to be 😜
That said, I did also eventually kinda grow some understanding at least of why Rust, as it is currently, does some things the way it does; I put my attempt at explaining my thoughts on that into another diagram 😜
But with the above apology towards Rust, I still can't stop feeling annoyed by it 😝 so still hoping that indeed things can be simplified in some "second system/generation" language based on lessons learnt by Rust 🤩 And keeping my fingers crossed for Garnet for that 😜🤞
But, in the end, whether to support some particular pattern in Garnet or not, is obviously your call and privilege to make 😃
"it feels like there's a smaller, simpler [language] hiding inside [Rust] and wanting to escape"
hell yeah.
I usually think of the tradeoff as "convenience vs. control". Maybe with "complexity" as a third independent axis. "Control" is usually what people area really talking about when they talk about performance; usually it's really about needing the control to improve performance where it's necessary. Some tasks almost can't be done without a high level of control, things like writing memory managers or operating system kernels. Also sometimes performance is about giving up control and letting the compiler make decisions about inlining or memory layout or such that are better than yours. Though I guess that's another sort of convenience; you could always do what the compiler does, you just have to do it awkwardly by hand because you can't explore the majillions of possibilities as quickly as the compiler.
"Safety" is yet another axis, which makes my mental graph a hypercube, which is where thinking of it as a scatter plot or such stops being very useful. :-P In your chart I'd maybe rename "security" to "safety", but that's nitpicking.
But ANYway, I first want to see how convenient Garnet can be just by trying to find the "smaller, simpler language hiding inside Rust", and then once we get that right we can add cosmetics. Cosmetics do matter; the syntax is the user interface for the language, and UI is important. It is annoying but important to realize that Garnet isn't really trying to be "a more convenient Rust that gives up some control". I think that languages in that category really really needs to exist; Swift is the main one I know of, but there's room for more. But Garnet's goal is "a smaller Rust that simplifies the hairy parts where possible".
One of Rust's unspoken rules, at least as I understand it, is "never make the programmer guess what type something is"; if you write
if x { ... }
then you know for a fact thatx
is abool
, not "bool or some other random falsey value that might be user-extendable at runtime". If you writex + y
then you know thatx
andy
are always numbers of the same type, or you look up theAdd
trait onx
's type to find out whaty
can be. You always know where theAdd
trait is allowed to be defined, you can't just conjure up new ones at runtime. To me this is one of the inconvenient things that is actually really useful for maintaining safety without giving up control;+
can almost always be monomorph'ed, it doesn't need runtime type checks or dynamic dispatch, etc. So I'm pretty hesitant to violate that unspoken rule without being pretty damn sure that it's worth the trouble.aaaanyway. This has turned into me rambling about philosophy. More convenient pattern matching or unwrapping would be nice to have.