HN user

filwit

10 karma
Posts0
Comments20
View on HN
No posts found.

There are many third-party libs which make using Vulkan much easier, all the way up to Game Engines which abstract everything. That isn't a argument against Vulkan.

A good graphics API, even a web one, is one that provides good control over the GPU.. and to have sane standards which limit future implementation fragmentation (like shader byte-code instead of a specific shader language), and to have good debugging tools, etc.. That's what Vulkan is, regardless of how verbose it is in comparison to OpenGL.

I'm not saying you can just make Vulkan run on the web, but I'm certainly in favor of a Vulkan-subset (using SPIR-V as a shader base) becoming the successor to WebGL over one inspired by Metal and MSL.

I'm not sure what you're implying.. you can turn on most optimizations and still keep nil-checks on in Nim (either the whole project via --nilChecks:one, or select portions of code via {.push.}).

Unless you're claiming your example was still hiting UB, even with nil-checks on, and just happened to throw an exception by random chance, I'm not really sure how you figure UB is still happening here (since the exception will be thrown, preventing the deref). Nothing is preventing you from using nil-checks in production code.

I should clarify: What's confusing me is the indexing stuff. I'm not sure if this is referring to something about the `Option<T>` or something else.

By indexing, I meant as an alternative to references.. For example, if you had a Sprite type which held a 'reference' to a Texture in your game's Texture list.. as soon as you allocate a Sprite it must borrow a reference to a Texture, preventing any future mutation of the Textures list for the lifetime of the Sprite, which obviously is too restricting for most games.. so the alternative is to have the Sprite simply hold an index to an item in the array instead, but this basically comes with the same pitfalls as nilable refs (ie, if you accidentally change it, your program can crash due to bounds-checking errors.. or end up with visual glitches.. not sure which would be more annoying).

The other alternative is to use an Option<&Texture> instead. However, I'm not familiar enough with Rust to know of the restrictions here, or even if that's possible (taking a look at the docs, it looks like it's possible, but life-time vars come into play, which could complicate things).

So I remembered correctly, Nim does not reach UB in non-release code (or rather, code without --boundCheck:on), it throws an exception. I still think this is a reasonable solution. We catch these errors during development iteration or enable it for safety-critical portions of release code (or the entire project).. and we can opt-out of these checks if we need the performance and safety isn't as important (games, simulations, etc).

I remember Rust does not bounds-check it's iterators, so you don't need to really disable bounds-checks (indeed, you cannot) while Nim, currently, does this more niavely and looses some performance for it. That's a nice thing Rust does, but not something Nim can't eventually catch up too. See this comparison for futher reference: http://arthurtw.github.io/2015/01/12/quick-comparison-nim-vs...

You prove to the compiler that the nilable var is not-nil via if statement. Eg:

  proc foobar(f:Foo not nil) =
    discard
  
  let f = Foo() # nilable ref
  let b: Foo not nil = f # Error, can't prove 'f' is not nil

  if f != nil:
    let b: Foo not nil = f # can assign non-nil vars to 'f'
    foobar(f) # or can pass 'f' directly

there are strictly more steps involved when you have null pointers.

Well yes, and both Nim and Rust have non-nil pointers.. I suppose I misread your original statement as "Rust is objectively better ..." when you actually just said non-nil vars are an objectively better design pattern in general. My mistake.

Our argument seems to stem around the two assertions (one from you, and one from me), those are: "nil vars are be rare (in optimally written code)", and "Rust's way of working with 'nil' vars is verbose". I suppose I'll concede that non-nil vars is a better default (though I will hold reservation until I see more real statistics, I don't find "no RFC yet!" as hugely convincing), but I also feel Rust could do a better job of giving access to "nilable" vars when they're needed.

I don't know what this means. Lifetimes rule out dangling pointers...

I mean, Rust prevents you (via compile-time mechanisms) from mutating a variable while it's borrowed by another reference.. If that reference is Option<>, it's only known at runtime weather or not a reference has actually borrowed said varaible. Rust must either treat every Option<> reference as a potential 'loan path', which would significantly diminish their usefulness as a references, encouraging indexing for these scenarios, which leads to almost identical potential for out-of-bounds crashes... or it's relying on some kind of more complex mechanism (lifetime vars maybe?).. or additional runtime overhead.

I really don't know enough about Rust to know how far off-base that is. So any clarity is appreciated.

In an earlier comment I was able to construct a Nim program that exhibited very different behavior in debug and optimized builds, using nothing but GC'd pointers.

I remember this comment, but I didn't remember it achieving UB in debug code.. I'll look through the history and take another look.

...because zebra's are not a universally useful modelling tool to programmers like references are. Thus, the absence of a reference, ie nil, also becomes a useful, commonly used modeling tool. If we all wrote software using african wildlife metaphor, 'non-zebra' might then be just as useful.

It's not verbose. "Option" is 6 characters. ".map" is 4.

I just want to note that verbosity isn't just about symbol length, but also about operator noise and the number of available or required commands used to achieve a goal. Just counting these characters isn't very relevant, and isn't even the best Rust can do (as someone pointed out you can use 'Some()' to get an Option var, which is only 4 chars).

That said, I agree this is rather subjective, and can't be well compared outside the context of the rest of the language.

No worries. I also wasn't implying you where trying to discourage Nim, and I hope my post didn't come off as accusatory. Cheers!

EDIT:

The solution here is to just use a reference instead of an arbitrary index.

Ah, sorry I should have said "mutable Texture array". In that case Rust's borrow-checking will 'freeze' the array, preventing Textures from ever being changed during the lifetime of your Sprites. So you're left with either Option<> or indexing as a solution, each with it's own merits, but neither as.. practical as nilable GCed references, IMO (again, just my opinion.. others seem to find it easy enough).

it is useful for the compiler to force you to handle the case in which pointers are null.

Well I agree that it's very useful (and we have that in Nim), but..

With constructs like Option::map the code is usually even less verbose than the equivalent code with null.

I'm still not convinced of this part. It certainly hasn't been the case with the, admittedly small amount of, Rust code I've seen. However, I'll look for more comparisons in the future (or offer Nim comparisons to Rust snippets anyone posts). Point is, nil is still a useful and commonly used tool. So the argument for verbosity and conveniences is relevant, IMO.

With null, the semantics of the language are that an exception can be thrown [1] whenever those constructs are invoked. That's pretty much objectively easier to reason about.

That completely depends on how often you want to use nil refs, and how easy they are to use. Like I said in another response, I agree Rust's design may be better for some domains, but I certainly wouldn't call it "objectively" easier to reason about in a general sense.

Huh? Lifetimes are totally independent.

Well like my post implied, I was only guessing as to the design. And it's interesting to hear that it takes advantages of special compiler optimizations. That said, I still don't see how it's completely decoupled from the life-time system.. you're saying that if I have a Option<> reference to a mutable list in Rust, the compiler can determine weather or not the list is 'frozen' based on the runtime state of that reference?

Or you could do what Nim does, and make dereferencing null undefined behavior.

I didn't think derefing nil was undefined behavior. I thought only dereferencing a pointer which points to once-valid-but-now-free memory was undefined behavior, and that situation is covered by GCed refs. Can you explain this a bit?

EDIT:

Not in my experience. They show up in production all the time.

I did say 'rarely', and I drew a comparison to bounds-check crashes, which surely also show up in production.

Rust's compiler prevents you from moving data into a method which then nulls it out

Just for clarity, we have this in Nim too, eg:

  type
    Foo = ref object
    Bar = object
      val: Foo not nil
  let
    f = Bar() # Error, 'val' must be set
  
  proc foobar(f: Foo not nil): Foo not nil =
    return nil # Error, can't return nil
  
  foobar(nil) # Error, can't pass nil
> At best this dangling pointer will look at garbage and cause a crash or undefined behavior. At worst, it will look at other, actively-used memory and cause a security vulnerability.

In C/C++, yes, but this isn't so applicable to Nim where we have GCed references and 'not nil' constraints.

If you use iterators in Rust, you never need to worry about out of bounds errors

Well I was not talking about iterating through a list, but rather maintaining arbitrary indexes to a mutable list. Eg, a Sprite which contains a index to a Texture array. In that scenario it's just as easy to miscalculate and crash your program via a bounds-checking error as it is to crash by nil-deref.

To offer a counter-viewpoint, I find that Option<> (and Result<>) are very easy to reason about..

It's good that Rust works for you, truly. And like I said in another post, I agree Rust's design here may be better for some domains. However, Nim's design still feels more elegant and straight-forward to me. Luckily, we both get a powerful language that suits us, regardless of which one we prefer :)

I agree the concept of 'non-nil' vars is very useful (and we have that in Nim), but I'm not entirely convinced by the rest of that argument. Namely, I don't agree that nil is rare enough to justify the verbosity Rust uses for it. Non-nil vars may be seen more often, but that doesn't mean nil vars aren't also often used, either. In Nim, both nil and non-nil vars are at roughly the same reach.. while in Rust non-nil vars are significantly easier work with. You may see that as a positive argument for Rust's safety (and you may be right for some domains), but I see it as more a negative argument for Rust's practicality.

So Rust has some cool safety features, especially for concurrent code. But, and perhaps I'm just uninformed, I never really understood the safety benefit of Rust's 'never nil' design. Nil is a useful modelling tool, even in Rust where it exists via Option<>/None, correct? Perhaps by forcing you to be extremely explicit (and enforcing `match` always handles all conditions) you gain some arguable safety, but at what cost? It's certainly not easier to use and reason about, IMO. And it seems just as likely you'll end up crashing your program due to a bounds-check error (which may happen more often since Rust encourages indexing over references due to this very design.. at least, so I've read).

It seems to me the design was chosen more as a way to ensure memory lifetime could be better predicted by the compiler rather than any strong argument for safety.. but then, I'm not well read on the subject, and It's very likely there's good safety arguments for it I'm not aware of.. either way, in my experience nil-deref errors are rarely a painful thing. They happen often, but are also fixed quickly.

Err... what you said just reminded me of something, and I realized all the code I just showed you is really over-complicated and that Nim has much more straight forward options using `const`, like this:

  static:
    # define a compile-time list first
    var names = newSeq[string]()
    
    # add some values (at compile-time)
    names.add("foo")
    names.add("bar")

  # define the compiler vars as run-time const
  const runtimeNames = names

  # define some run-time variables
  var name1 = "foo"
  var name2 = "blah"

  # check runtime variables against const variable
  if runtimeNames.contains(name1): echo "Has Foo!"
  if runtimeNames.contains(name2): echo "Has Blah!"
Sorry about the rather winded (and bad example) replys :| But thanks for the conversation, it reminded me of this and now I have some cleaning up of my own code to get too. Cheers!

can static: sections assign to an array that will be available at runtime?

The answer is yes, but it's a tad trickier than just accessing the compile-time list from run-time code (which doesn't make sense, and is illegal). Instead, use a macro to generate a bunch of run-time checks against a specific value. Eg:

  var eventNames {.compileTime.} = newSeq[string]()
  
  proc defineEvent(name:static[string]) =
    static:
      eventNames.add(name)
  
  macro checkDefined(name): stmt =
    # begin new statement
    result = newStmtList().add quote do:
      echo "Checking for '", `name`, "'"
    
    # loop over every known event name and
    # build a run-time 'if' check for each one.
    for n in eventNames:
      result.add quote do:
        if `n` == `name`:
          echo "Found it!"
  
  
  # add some events to compile-time list
  defineEvent("foo")
  defineEvent("bar")
  
  # define some runtime values
  let eventName1 = "foo"
  let eventName2 = "blah"
  
  # check runtime values againts compile-time list
  checkDefined(eventName1)
  checkDefined(eventName2)
  
  # output:
  #   Checking for 'foo'
  #   Found it!
  #   Checking for 'blah'
Note: This will inject a bunch of 'if' statements for each call to 'checkDefined', which might bloat your code.. it's probably better to make a macro which defines a proc, then just call that to check run-time values.. but I left those kinds of details out of this illustration for the sake of simplicity.

I'm not sure exactly what you're asking, but I can answer at least part of it. You can build lists at compile time in Nim via the `static` statement or `compileTime` pragma. Eg:

  # using the pragma here, but we could use a 'static' block instead
  var cycles {.compileTime.}: int

  # ---

  proc doSomething =
    static:
      # invoked once at compile-time (at this definition)
      cycles += 1

  proc doSomethingGeneric[T] =
    static:
      # invoked once at compile-time (per unique generic call)
      cycles += 1

  macro doSomethingAtCompileTime(n): stmt =
    # invoked at compile-time (per call)
    let ncycles = int n.intVal
    cycles += ncycles

  # ---

  doSomething() # this call doesn't effect 'cycles', it's declaration does (+0)
  doSomething() # ditto (just here to prove a point) (+0)
  doSomethingGeneric[int]() # this call effects 'cycles' (+1)..
  doSomethingGeneric[int]() # ..but only once (+0)
  doSomethingGeneric[float]() # this call also effects 'cycles'  (+1)
  doSomethingAtCompileTime(5) # this call effects 'cycles'  (+5)
  doSomethingAtCompileTime(12) # ditto  (+12)

  static:
    echo cycles # prints '20'
I'm not sure this helps solve anything in NimES, but this can be really useful for meta-programming in general. For instance I'm using it to make an event system which generates efficient dispatch lists based on the types/procs defined. It's designed for game logic, to both minimize boiler-plate and dynamic dispatch. Ie, invocation it's type-specific and usually does not use function pointers per-instance, so smaller 'events' can be inlined. Plus, instances are generic (decoupled from object inheritance), and often don't require any header data. That combination should, in theory (still WIP), give devs a 'universal' way to model a broad range of game objects from heavy single-instance types to lightweight projectiles and particles. Here's an example for a little clarity:
  # note: 'impl', 'spawn', and 'invoke' are macros
  
  type
    Foo = object
      val: int
    
    Bar = object
      val: string
  
  impl Foo:
    proc update = echo "Foo: ", me.val
    proc render = echo "Rendering Foo!"
  
  impl Bar:
    proc update = echo "Bar: ", me.val
  
  spawn Foo(val:123)
  spawn Bar(val:"abc")
  
  invoke update
  invoke render
  invoke blah
  
  # ouptput:
  #   Foo: 123
  #   Bar: abc
  #   Rendering Foo!
  #   Invoke Error: No event 'blah' defined.
Perhaps that's not the best example to illustrate Nim's meta capabilities, but so far Nim is the only language I've come across that allows me to achieve this short of thing (at least, directly from user-code).

Not that it's even really needed... Just select the section of text and use your IDE's shortcuts to comment it out (which now works fine in any IDE due to comments no longer being part of the AST)

Ridiculous.

We use "special tools" for every other language (Visual Studios, Eclipse, etc).. Calling a language feature, which give programmers style freedom, a "tortured lexical structure" is not an objective argument. Especially since we have a tool (nimgrep) which addresses the issue and takes < minute to learn. Once Nim has better IDE support, no one will be grepping in the first place.

In practice, resolving symbols is not really harder. Once you understand the symbols rules, your mind is very good at finding the match. Plus the real solution is proper IDE support with "goto definition". IMO, the benefits of semi-case-insensitivity (allow programmers to write how they want) outweight the issues it causes.