Hacker News new | past | comments | ask | show | jobs | submit | mraleph's comments login

At some point in my life (when I briefly worked on LuaJIT for DeepMind) I have written a stack walker which can stitch together native and Lua frames: for each native stack frame it checks if that is actually an interpreter frame or a trace frame - if that's the case it finds corresponding `lua_State` and unwinds corresponding Lua stack, then continues with native stack again.

This way you get a stack trace which contains all Lua and native frames. You can use it when profiling and you can use it to print hybrid stack traces when your binary crashes.

I was considering open-sourcing it, but it requires a bunch of patches in LJ internals so I gave up on that idea.

(There is also some amount of over-engineering involved, e.g. to compute unwinding information for interpreter code I run an abstract interpretation on its implementation and annotate interpreter code range with information on whether it is safe or unsafe to try unwinding at a specific pc inside the interpreter. I could have just done this by hand - but did not want to maintain it between LJ versions)


> - it is truly native, in the sense I can directly talk to native APIs on each platform without going through bridging code and there's no special runtime

I thought Compose Desktop is running on JVM an renders through Swing/AWT? Did this change?


You are correct, that part of my comment was more a feature Kotlin multiplatform which Compose is built on top of. When building with Compose it has been a really nice experience to just call out to Foundation APIs on iOS in the same way I can from Swift, which has been the most impressive part to me. In Compose for Desktop apps you use JNA for native access IIRC. Kotlin multiplatform itself can compile natively to macOS, Windows and Linux without JVM, but you'd be running in a separate process to your Compose app which is JVM based on desktop.

Apologies my original comment was unclear on that.


The reason why languages promote variable types based on control flow is because developers en masse actually expect that to happen, e.g. facing the code like

    Dog? maybeDog;
    if (maybeDog != null) {
      maybeDog.bark();
    }
If compiler says "sorry, maybeDog might be null", developer (rightfully so) usually responds "but I have just checked and I know it is not, why do you bother me?". So languages chose to accommodate this.

> What if I want to set the value back to nil if it is not-nil?

You can. The type of the variable does not actually change. You could say that the information about more precise type is simply propagated to the uses which are guarded by the control flow. The following will compile just fine:

    Dog? maybeDog;
    if (maybeDog != null) {
      maybeDog.bark();
      maybeDog = null;
    }
> Why should I have to wrap things back in an optional if I want to pass it along as such?

You don't, with a few exceptions. There is a subtype relationship between T and T?: T <: T?, so the following is just fine:

    void foo(Dog? maybeDog);

    Dog? maybeDog;
    if (maybeDog != null) {
      maybeDog.bark();
      foo(maybeDog);  // Dog can be used where Dog? is expected
    }
 
You might need to account for it in places where type inference infers type which is too precise, e.g.

    Dog? maybeDog;
    if (maybeDog != null) {
      // This will be List<Dog> rather than List<Dog?>. 
      final listOfDogs = [maybeDog];
    }
Though I don't think it is that bad of a problem in practice.


See, I don't think languages should accommodate this, because I see it as an ugly solution. It's nice that it works in a few cases but then it very quickly breaks down: a developer finds that their null check is enough to strip an optional, but a test against zero doesn't convert their signed integer to unsigned. Checking that a collection has elements in it doesn't magically turn it into a "NonEmptyCollection" with guaranteed first and last elements. I'm all for compilers getting smarter to help do what programmers expect, but when they can't do a very good job I don't find the tradeoff to be very appealing. Basically, I think pattern matching is a much better solution this problem, rather than repurposing syntax that technically means something else (even though 90% of the time people who reach for it mean to do the other thing).

Also, fwiw, I was mostly talking about things like the last example you gave. I guess it would be possible that in circumstances where T is invalid but T? would be valid, the language actually silently undos the refinement to make that code work. However, I am not sure this is actually a positive, and it doesn't help with the ambiguous cases anyways.


This is the elegant solution.

Where does it break down?

This isn't the same thing as "magically" changing the type.

What does the syntax technically mean?

What refinement is undone?

I think you're a bit too wedded to the idea that there's a type conversion going on or some dark magic or something behind the scenes that's "done" then "undone" like a mechanism. There isn't. It's just static analysis. Same as:

    let x: Animal;
    x = Dog()
    if (x is Dog) { 
      x.bark()
    }

The Zen koan you want to ponder on your end is, why do you want to A) eliminate polymorphism from OOP B) remove the safety of a compiler error if the code is changed to x = Cat()?


I don’t like that either. x is Dog is a boolean expression. Presumably I can write

  let isDog = x is Dog;
And the value whether it is a dog or not goes into that new variable. The fact that I can’t then immediately go

  if (isDog) {
     x.bark()
  }
shows the deficiencies of this static analysis (even if you could do some simple value tracking to make this work, it doesn’t really address the real issue I have with it, which I described above: why can’t I do this refinement for other things?) The conceptual model is one of “we special cased 2-3 cases that we can detect”, which I don’t find very elegant, especially considering that other languages seem to have better solutions that express the operation in what I feel is a better way.

(The equivalent Swift code would be something like this:

  let x = Animal()
  if let dog = x as? Dog {
      dog.bark()
  }
I see this as much superior. One, because x is still available in the scope of I want an Animal and not a Dog for whatever reason. I understand that most of the time you don’t do this but to have it available and not have to do a special case for it is nice. The second thing is that I just like the syntax, since it gives you an opportunity to give a more specific name in that branch as part of the assignment. Third, it composes, because none of the operations are special save for the “if let” binding. This code is also valid:

  let x = Animal()
  let dog /* : Dog? */ = x as? Dog
  if let dog /* = dog */ {
  }
The static analysis for undoing polymorphism is the exact same as the one for binding to optionals, because the idiomatic way to cast results in an optional.)


> The fact that I can’t then immediately go

Who said you can't? :) This actually works in Dart:

    Dog? dog;
    bool isDog = dog is Dog;
    
    if (isDog) {
      dog.bark();
    }
i.e. boolean variables which serve as witnesses for type judgements are integrated into promotion machinery.

I do agree with you that

1. In general there is no limit to developers' expectations with respect to promotion, but there is a limit to what compiler can reasonably achieve. At some point rules become too complex for developers to understand and rely upon. 2. Creating local variables when you need to refine the type is conceptually simpler and more explicit, both for language designers, language developers and language users.

I don't hate excessive typing, especially where it helps to keep things simple and readable - so I would not be against a language that does not do any control flow based variable type promotion. Alas many developers don't share this view of the world and are vehemently opposed to typing anything they believe compiler can already infer.

It's all a matter of personal taste in the end.


Kotlin can't do this, which the language I have more experience with. It's good that Dart does a little better in that regard. And, I think we do agree, I just don't really like the viewpoint of doing this, because I feel like it's not really general enough.


I don't think any of that has to do anything with static analysis deficiencies. The analysis is the same. What Swift requires is for the user to manually help the compiler with a rather verbose and otherwise unnecessary construct.

Type narrowing is not a new idea


It’s not a new idea, but I don’t think it’s a good idea. That’s just it. Swift has a construct to do the equivalent and it’s fewer characters to boot when compared to Dart when you’re going from an optional to non-optional type.


But that's exactly what Swift does: it narrows the types. To do that, it uses a separate construct that only exists to make the parser ever so slightly simpler.


Yes and I think having a separate construct rather than using another one and imbibing it with new meaning is more elegant


The ugly solution is Swift's if let x = x which is literally the same check, but in a more verbose manner.

Yes, compilers should be able to help in this and many other cases, and not just give up and force the programmer to do all the unnecessary manual work


It’s not a check, it’s binding a new variable with a different type (which may or may not have the same name; if it does you can use if let x these days). And solving the other examples I mentioned is generally out of the realm of most programming languages in use today (except maybe TypeScript?) It comes with significant challenges that I’m not sure we are ready to address properly yet.


In Dart 3 you have patterns and sealed class families to achieve that. See my comment above[1].

The syntax is unfortunately more verbose, but if all goes well we will address that issue this year.

[1]: https://news.ycombinator.com/item?id=39613415


In Dart 3 instead of declaring the variable and then comparing you can simply write an if-case pattern:

    if (value case final value?) {
      // value is a fresh final variable 
    }


So Dart is moving in the Swift direction, which has

    if let value = value {
      // a new binding named ‘value’ refers to value, unwrapped 
    }
or (newer syntax that I initially found even weirder than the older one, but both grew on me)

    if let value {
      // a new binding named ‘value’ refers to value, unwrapped 
    }
(The old version will remain, as it is more flexible, allowing such things as

    if let baz = foo.bar(42) {
      // a new binding named ‘baz’ refers to foo.bar(42), unwrapped 
    }
)


The more you know, I had totally missed this.


In Dart you use class hierarchies instead, rather than enums (which in Dart are way to define a compile time constant set of values). So the original example becomes:

    sealed class MySomething<T> {
    }

    final class One extends MySomething<Never> {
    }

    final class Two extends MySomething<Never> {
      final Child child; 
      
      Two(this.child);
    }

    final class Three<T> extends MySomething<T> {
      final T value;

      Three(this.value);
    }

    final class Four extends MySomething<Never> {
      final int Function() callback;

      Four(this.callback);
    }
And then you can exhaustively switch over values of MySomething:

    int foo(MySomething<String> foo) {
      return switch (foo) {
        One() => 1,
        Two(child: Child(:final value)) => value,
        Three(:final value) => int.parse(value),
        Four(:final callback) => callback(),
      };
    }
The declaration of a sealed family is considerably more verbose than Swift - and I really would like[1] us to optimized things for the case when people often declare such families. Macros and a primary constructors will likely provide reprieve from this verbosity.

But importantly it composes well with Dart's OOPy nature to achieve things which are not possible in Swift: you can mix and match pattern matching and classical virtual dispatch as you see fit. This, for example, means you can attach different virtual behavior to every member of the sealed class family.

[1]: https://github.com/dart-lang/language/issues/3021


> Dart+Flutter look promising until you notice that they run Skia WASM inside the dart VM which itself runs in WASM which runs inside a JS VM and performance is abysmal.

That's not how Flutter works on either of the platforms it supports.

When you run natively (e.g. mobile, desktop, etc) Skia or Impeller execute natively and all your Dart code is compiled ahead-of-time to native code as well. No JS or Wasm is involved anywhere in the stack. No JIT compilation in release binaries, only during development.

When you run on the Web - then naturally Skia is compiled to Wasm and Dart code is compiled to JS or Wasm, JS VM will end up running both of those. No Dart VM is involved anywhere here.


When I profile the (excruciatingly slow) demo website

(EDIT: WARNING that link crashes Firefox on my phone)

https://superdash.flutter.dev/

it sure looks like there are parts of the Dart VM in the call stack. Maybe I should have said Dart runtime?

But anyway, the dart example app stutters on my Android phone and the website stutters on my quad-core laptop with a mobile 3080. And it's basically just tappy chicken, so a simple 2D side scroller.


Not my comment, but relevant here "The problem with compiling Skia to WASM is you'll lose any benefits of hardware graphics acceleration on the device."

(From https://github.com/AvaloniaUI/Avalonia/discussions/6831#disc... )


These examples for the flutter rpg game engine seem to run great on my old pixel 3, under the hamburger menu > mini games:

https://bonfire-engine.github.io/examples/bonfire-v3


That "map by tiled" example feels like 10 fps on my Firefox mobile. It's significantly better than the official Flutter demo game, but significantly worse than a pure JS framework.


[disclaimer: I work on Dart, though not on the language team]

With primary constructors this will become something along the lines of:

    sealed class Message();

    class IncrementBy(final int value) extends Message;

    class DecrementBy(final int value) extends Message;

    class Set(final int value) extends Message;
Which is considerably less repetitive, though `extends Message` is still there. I am fairly optimistic that the next step would be to eliminate that[1], though I think we would need to gather a bit more feedback from users as people are getting more and more reps in with Dart 3.0 features. I personally would prefer something along the lines of:

    sealed class Message() {
      case IncrementBy(final int value);
      case DecrementBy(final int value);
      case Set(final int value);
    }
Current syntax is not all that bad if you are going to do OO and add various helper methods on `Message` and its subclasses, but if you just want to define your data and no behavior / helpers - then it is exceedingly verbose.

[1]: https://github.com/dart-lang/language/issues/3021


That syntax does look a lot nicer and makes sense if you're going to do OO anyway. I do wonder if it's trying to shoe horn OO into functional clothing but seems practical nonetheless. Primary constructors would definitely be a welcome change.


It is way easier for me to read than what you proposed in Typescript, more characters to write? sure but how many times do I have to write a piece of code and how many times do I have to read it?

I guess ultimately it depends on which language you are more familiar with...


I'm more than happy with either of those variations. My favourite though would be Elm's syntax ;)

    type Message 
        = IncrementBy Int
        | DecrementBy Int
        | Set Int


One big difference between how Dart (and other OO languages that take a similar approach) and what Elm and other functional languages do is that in Dart, each of the cases is also its own fully usable type.

In Dart, if you do:

    sealed class Message {}

    class IncrementBy extends Message {
      final int amount;
      IncrementBy(this.amount);
    }

    class DecrementBy extends Message {
      final int amount;
      DecrementBy(this.amount);
    }

    class Set extends Message {
      final int amount;
      Set(this.amount);
    }
You can then write functions that accept specific cases, like:

    onlyOnIncrement(IncrementBy increment) {
      ...
    }
In Elm and friends, IncrementBy is just a type constructor, not a type. Further, we can use the class hierarchy to reuse shared state and behavior in a way that sum types don't let you easily do. In your example, each case has an int and it so happens that they reasonably represent roughly the same thing, so you could do:

    sealed class Message {
      final int amount;
      Message(this.amount);    
    }

    class IncrementBy extends Message {
      IncrementBy(super.amount);
    }

    class DecrementBy extends Message {
      DecrementBy(super.amount);
    }

    class Set extends Message {
      Set(super.amount);
    }
And now you can write code that works with the amount of any Message without having to pattern match on all of the cases to extract it:

    showAmount(Message message) {
      print(message.amount);
    }
So, yes, it's more verbose than a sum type (and we do have ideas to trim it down some), but you also get a lot more flexibility in return.


In an FP code base, it’s common for ADTs to be the backbone of all data structures from Maybe to Either, to anything you use to model data types. That being the case changing 4 lines into 13 scaled into a entire code base is a massive amount of sludge to wade thru & one of the best way to up the code quality is to increase readability for maintainers. More LoC is more LoC to maintain even if it seems like it’s just a few more lines.


> changing 4 lines into 13 scaled into a entire code base is a massive amount of sludge to wade thru

It's only more verbose for the code that is defining new types. Code that is simply defining behavior (either in functions or methods) is unaffected and my experience is that that's the majority of code.


But reading those definitions at the top of the file is usually one of the first places I’m going to go to understand what’s going on. The 4-line option to me is much easier to grok, not only because it’s more terse but the way the pipes are stacked to indicate visually that they are related to the same underlying structure. The `extends Message` bit is a level of indirection that requires the reader to juggle a lot more in their head (as is immutable `final` not being the default). The `class` keyword also carries a lot of baggage that it’s not clear what to expect (will this have methods or not?, will we be seeing `this`?, etc.).


I think you're evaluating this as a notation for defining sum types. But that's not what it is. It's a notation for defining a class hierarchy, with all of the additional affordances that gives you.

In practice, most real-world Dart code that uses sealed types also defines methods in those types, getters, and all sorts of useful stuff. Once you factor that in, the data definitions themselves are a relatively small part.

(Of course, you could argue that defining class hierarchies is already intrinscally bad. But Dart is an object-oriented language targeting people that like organizing their code using classes.)


This is definitely an FP vs OO thing. If you wanted to refer to the value in an ADT you'd introduce a new type to refer to it, which in Elm would be:

    type Message 
        = IncrementBy Amount
        | DecrementBy Amount
        | Set Amount

    type Amount = Amount Int
Obviously this is a contrived example - you wouldn't bother if you were dealing with an Int but once the message gets more complicated it can make sense.


That's a different thing.

Given the above ADT, how would you write a function that prints the amount of a Message, regardless of which case it is?


You'd use a switch over the ADT and extract the values as appropriate. This is method dispatch vs function dispatch. In practice you can do the same things with either, but they reflect the focus on behaviour (OO) vs data (FP).


That's the point of my last example. By building this on subtyping, you can hoist that shared property out of the cases and up to the supertype. That lets you define state and behavior that is case independent without having to write a bunch of brain-dead matches that go over every case to extract conceptually the same field from each one.

Of course, you can also reorganize you datatype so that the shared state is hoisted out into a record that also contains a sum typed field for the non-shared stuff. But that reorganization is a breaking change to any consumers of the type.

Modeling this on top of classes and subtyping lets you make those changes without touching the user-visible API.


I appreciate you taking the time to answer, but honestly the boilerplate is just a killer. I don't care about the additional flexibility when the code is so much harder to make sense of, and this is just a toy example.


fp-ts has this same unfortunate issue (https://gcanti.github.io/fp-ts/guides/purescript.html#data) where a PureScript one-liner becomes 11 lines of boilerplate TypeScript. Or (https://gcanti.github.io/fp-ts/guides/purescript.html#patter...) 3 lines of pattern matching becomes 10 where the matching is done on strings (ad-hoc _tag property).


> it was built on top of v8

Was never built on top of V8 in any shape of form. Dart VM does not even share any code with V8.

Dart 1 was designed by people who originally started V8 (Lars Bak and Kasper Lund), that's the only connection.


Seems I too may be misremembering as well, I do know there used to be a custom Chromium build that included a Dart runtime so I assumed that was why v8 was used. But I remember now, there was an original effort to include dart in Chrome which met no fanfare.


Dart does not have arrays, it has lists. Try replacing "Dart array" with "Dart list" instead.


Arrays are also somewhat unusual in Rust (because they are fixed-length, unlike vector "Vec").

Why not use "function" or "loop" or something universal as the keyword?


For new experimenters of languages, "array" is the perfect word to use. It's a very common word that everyone learns whenever they start with C or Java. All the people looking to try things will use that term first before learning the proper terms.

Someone who never used rust will search for "rust array". Someone who did will search for "rust vec". You're trying to figure out trends for people who have never used the language.


> You're trying to figure out trends for people who have never used the language.

But you're not filtering out experienced users in the other languages, where "array" is a common term for all levels. So that argument doesn't hold either, unless you included other keywords like "what is" or "how do I" or "what's the equivalent to" to target learners no yet accustomed to the terminology.


This just gives you results for people who know that dart has lists, therefore, not people currently learning and experimenting the language.


Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: