Comment by nu11ptr

9 months ago

As much as I love Rust I sometimes wonder if I'd be more productive in a simpler language. If I wrote it every day I'm not sure that would be true, but as a hobbyist coming back to Rust sometimes takes me a bit to get back in the zone. Also, still not a fan of async, as it is woefully incomplete and fairly complicated in some use cases. That said, I just can't go back to Go with nil pointers and lack of decent enums/ADTs/pattern matching either. I long for the "in between" language, but with an amazing 3rd party ecosystem as both Rust/Go have.

NOTE: I'm not a game dev

Maybe people will make fun of me, but I've been very happy with Kotlin and Dart. Null-safe, good ergonomics, very fast.

I've tried Rust, sometimes play with C, D, Deno/TS, Nim, Java (actually I still write lots of it) and even some more cutting-edge stuff, like Unison. While they're cool, what I want is a language with really good tooling that gets out of my way without letting me write patently dumb code (like Java lets me use any object without checking for null, when it can be null but the language just doesn't give a shit to help me).

I use Dart when I want to compile to binary executable or use Flutter, and Kotlin for stuff I think the JVM has more to offer, like a server. The two languages are just a pleasure to use, pretty similar but having completely different ecosystems (which is great, you can use the best one for the job!).

  • I'm glad you found tools/languages that work for you. Kotlin felt a little too much like Java to me. If I stuck with a JVM lang. I'd probably go back to Scala 3, but I just don't like the JVM as a user (just sucks too many resources).

  • I wrote a Flutter package[0] that wraps the Filament 3D renderer, which I used to make a mini game for a Flutter game competition:

    https://devpost.com/software/escape-from-heat-island

    (Judging is still ongoing and votes would really be appreciated! It would help me to get more resources to work on the underlying package).

    This was my first ever “game” (tech demo, really), and I’m not a game dev, so take this with a grain of salt - but I do think there’s a lot of potential for Flutter/Dart as a game framework. Hot reload makes iterating on game logic very fast, you obviously get the UI toolkit and cross-platform support straight out of the box, and the language itself is (relatively) concise, so it lends itself well to gameplay programming. When you need to get your hands dirty at a lower level, you just drop down to C++ (or whatever engine you can expose via FFI).

    I think Google believe that Flutter can nab market share from Unity in casual 2D games (hence their official sponsored competition), but I think it has even more potential than that. In fact, I’ve seen at least two game companies (Supercell and another whose name I’ve forgotten) hiring for people to work on embedding the Flutter engine in various platform games.

    [0] https://github.com/nmfisher/flutter_filament.git

  • I like Kotlin but I find the fact that it lacks a good way to detect and handle possible errors very frustrating. If some function can fail on sane-looking input I'd like to know about it

OCaml could be that language.

I’m not convinced that ecosystem is so important for game dev. Once you have a simple graphics library, bindings to BulletPhyiscs etc most of the code is custom simulation code with no integrations needed.

  • I can echo OCaml. It is probably the most underrated language imo. It has great compilation times, macros, type inference, good tooling. With few exceptions, the library ecosystem doesn’t suffer from the same overengineering issues as Haskell and types are kept relatively simple. It has simple runtime characteristics making it easy to optimize performance when needed, although it tends to be very fast in general.

    • last I used OCaml, the standard library situation was abysmal (compared to say Haskell's), and you had to go searching for third-party "batteries included" crates to cover simple stuff. Has that gotten any better the last few years?

> I just can't go back to Go with nil pointers and lack of decent enums/ADTs/pattern matching either.

Go is simply a badly designed language where the idea of "simplicity" has been maligned and proven bad ideas likes nil/null, exceptions and such have been introduced in a seemingly modern language. One would think that decades of Java, Javascript, etc. code blowing up because of this issues would teach someone something but seems that is not always the case.

  • Having worked with Go for about a decade now, I largely agree that nil is a pain in the ass to work with, and the language has largely done nothing to make it better. However, Go (mostly) doesn't have exceptions. Ordinary problems are represented by non-nil errors, which are values. Panics exist but really are reserved for exceptional situations (with the number 1 cause being, of course, dereferencing nil).

  • "code blowing up because of this issues"

    I ran into these issues all the time with Java, C++, and Python projects.

    But it's just not the experience of running Go in production, which I've been doing for over 10 years now, across many projects with many devs.

    In practice, nil checks are just not very difficult to include everywhere. And experienced Go programmers don't use exceptions (panic/recover) almost ever.

    • What you said is:

      1) Anecdotal

      2) Based on faith that someone will not forget to do something instead of a well documented mechanism in the language that could block that from the start

      Having nil/null to handle empty references is simply very bad design and there's decades of examples why. The correct way is using a two-value type like Option, Maybe, etc. so that the (possibility) of the value missing is actually encoded in the type system

  • And yet it is incredibly productive. The poster that contrasted engineers with artists got it right I think. Go is an engineer’s language.

    • > Go is an engineer’s language.

      No, Ada/Spark is an example of a good engineers language. Go is a mediocre effort at best. Rob Pikes defence is that it was designed for junior Googlers who "aren't capable of understanding a brilliant language". Yes that's a real quote.

      4 replies →

I've been hoping to see more languages that compile to Go, it might be the most practical way to arrive at what you want. For example, see Borgo: https://news.ycombinator.com/item?id=36847594

As much as I dislike many of the legacy issues with JavaScript I find TypeScript to be the best language to iterate with. If Rust had GC without need for wrappers like RC though I think that would be my preferred iteration language. I mostly try to write my TS in a manner that would translate to Rust, but that's hard to do sometimes when it comes to memory management.

It doesn't occupy the same space, but the simplicity of Gleam has been very enjoyable to me. It's still quite a young language though, but worth keeping an eye on.

For me, the closest language currently is F#.

The open-source ecosystem is not as massive as Go's or the JVM's, but it's not niche either. F# runs on .NET and works with all .NET packages (C#, F#, ...). If the .NET ecosystem can work out for you, I recommend taking a closer look at F#.

F# allows for simple code, which is "functional" by default, but you're still free to write imperative, "side-effectful" code, too.

I find this pragmatic approach works extremely well in practice. Most of my code ends up in a functional style. However, once projects grow more complex, I might need to place a mutable counter or a logging call in an otherwise pure function. Sometimes, I run into cases where the most straightforward and easy to reason about solution is imperative.

If I were confined to what is often described as a "pure functional" approach, I'd have to refactor, so that these side-effects would be fully represented in the function signature.

F# ticks the enums/ADTs/pattern box but also has its own interesting features like computation expressions [0]. I would describe them as a language extension mechanism that provides a common grammar (let!, do!, ...) that extension writers can target.

Because of this, the language doesn't have await, async or any other operators for working with async (like C# or TS). There's an async {} (and task {}) computation expression which is implemented using open library methods. But nothing is preventing you from rolling your own, or extending the language with other computation expressions.

In practice, async code looks like this:

  let fetchAndDownload url =
    async {
        let! data = downloadData url // similar to C#: var data = await downloadData(url);

        let processedData = processData data

        return processedData
    }

I often use taskResult{}/asyncResult{}[1] which combine the above with unwrapping Result<>(Ok or Error).

Metaprogramming is somewhat limited in comparison to Scala or Haskell; but still possible using various mechanisms. I find that this isn't a big issue in my work.

IDE-wise, JetBrains Rider is a breeze to work with and it has native F# support. There is also Visual Studio and VS Code with Ionide, which are better in some areas.

You can run F# in Jypiter via .NET Interactive Notebooks (now called "Polyglot Notebooks" [2]). I haven't seen this mentioned often, but this is very practical. I have a combination of Notebooks for one-off data tasks which I run from VS Code. These notebooks can even reuse code from my regular F# projects' code base. Over the past years, this has almost eliminated my usage of Python notebooks except for ML work.

[0]: https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...

[1]: https://github.com/demystifyfp/FsToolkit.ErrorHandling?tab=r...

[2]: https://marketplace.visualstudio.com/items?itemName=ms-dotne...

  • Do you know of or have any shareable (sample) projects implemented in your way of doing F#? It sounds very intriguing to me

    • While the libraries and techniques I mentioned above seem to be well-known, I couldn't find a good public sample project.

      I can recommend https://fsharpforfunandprofit.com/ as a starting point.

      If there's interest, I can split some of my code into stand-alone chunks and post my experience of what worked well and what didn't.

      I wanted to share some thoughts on here on what brought me to F#. Maybe this can serve as a starting point for people who have similar preferences and don't know much about F# yet.

      A big part that affects my choice of programming language is its type system and how error handling and optionality (nulls) are implemented.

      That "if it compiles, it runs" feeling, IMO, isn't unique to Rust, but is a result of a strong type system and how you think about programming. I have a similar feeling with F# and, in general, I am more satisfied with my work when things are more reliable. Avoiding errors via compile-time checks is great, but I also appreciate being able to exclude certain areas when diagnosing some issue.

      "Thinking about programming and not the experience" the author lamented in the blog post appears to be the added cost of fitting your thoughts and code into a more intricate formal system. Whether that extra effort is worth it depends on the situation. I'm not a game developer, but I can relate to the artist/sound engineer (concept/idea vs technical implementation) dichotomy. F#'s type system isn't as strict and there are many escape hatches.

      F# has nice type inference (HM) and you can write code without any type annotations if you like. The compiler automatically generalizes the code. I let the IDE generate type annotations on function signatures automatically and only write out type annotations for generics, flex types, and constraints.

      I prefer having the compiler check that error paths are covered, instead of dealing with run-time exceptions.

      I find try/catches often get added where failure in some downstream code had occurred during development. It's the unexpected exceptions in mostly working code that are discovered late in production.

      This is why I liked Golang's design decisions around error handling - no exceptions for the error path; treat the error path as an equal branch with (error, success) tuples as return values.

      Golang's PL-level implementation has usage issues that I could not get comfortable with, though:

        file, err := os.Open("filename.ext")
        if err != nil { return or panic }
        ...
      

      Most of the time, I want the code to terminate on the first error, so this introduces a lot of unnecessary verbosity.

      The code gets sprinkled with early returns (like in C#):

        public void SomeMethod() {
        if (!ok) return;
        ...
        if (String.IsNullOrEmpty(...)) return;
        ...
        if (...) return;
        ...
        return;
        }
      

      I noticed that, in general, early returns and go-tos introduce logical jumps - "exceptions to the rule" when thinking about functions. Easy-to-grasp code often flows from input to output, like f(x) = 2*x.

      In the example above, "file" is declared even if you're on the error path. You could write code that accesses file.SomeProperty if there is an error and hit a null ref panic if you forgot an error check + early return.

      This can be mitigated using static analysis, though. Haven't kept up with Go; not sure if some SA was baked into the compiler to deal with this.

      I do like the approach of encoding errors and nullability using mutually exclusive Result/Either/Option types. This isn't unique to F#, but F# offers good support and is designed around non-nullability using Option types + pattern matching.

      A possible solution to the above is well explained in what the author calls "railway oriented programming": https://fsharpforfunandprofit.com/posts/recipe-part2/.

      It's a long read that explains the thinking and the building blocks well.

      The result the author arrives at looks like: let usecase = combinedValidation >> map canonicalizeEmail >> bind updateDatebaseStep >> log

      F# goes one step further with CEs, which transform this code into a "native" let-bind and function call style. Just like async/await makes Promises or continuations feel native, CEs are F#'s pluggable version of that for any added category - asynchronicity, optionality, etc..

      With CEs, instead of chaining "binds", you get computation expressions like these: https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoo... https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoo...

      Everything with an exclamation mark (!) is an evaluation in the context of the category - here it's result {} - meaning success (Ok of value) or error (Error of errorValue). In this case, if something returns an Error, the computation is terminated. If something returns an Ok<TValue>, the Ok gets unwrapped and you're binding TValue.

      I have loosely translated the above example into CE form (haven't checked the code in an editor; can't promise this compiles).

        let useCase (input:Request) =
         result {
            do! combinedValidation |> Result.ignore
            // if combinedValidation returns Result.Error the computation terminates and its value is Result.Error, if it returns Ok () we proceed
            let inputWFixedEmail = input |> canonicalizeEmail
            let! updateResult = updateDatabaseStep inputWFixedEmail // if the update step returns an Error (like a db connection issue) the computation termiantes and its value is Result.Error, otherwise updateResult gets assigned the value that is wrapped by Result.Ok
            log updateResult |> ignore // NOTE: this line won't be hit if the insert was an error, so we're logging only the success case here
            return updateResult
         }
      

      In practice, I would follow "Parse, don't validate" and have the validation and canonicalizeEmail return a Result<ParsedRequest>. You'd get something like this:

        let useCase input =
         result {
            let! parsedUser = parseInput input
            let! dbUpdateResult = updateDatabase parsedUser 
            log dbUpdateResult |> ignore
            return updateResult
         }
      
        let parseInput input =
         result {
            let! userName = ...
            ...
            return { ParsedRequest.userName = userName; ... } // record with a different type
         }
      

      This setup serves me well for the usual data + async I/O tasks.

      There has been a range of improvements by the F# team around CEs, like "resumable state machines" which make CEs execute more efficiently. To me this signals that CEs are a core feature (this is how async is supposed to be used, after all) and not a niche feature that is at risk of being deprecated. https://github.com/fsharp/fslang-design/blob/main/FSharp-6.0...

      1 reply →

Isn't that just Swift/Kotlin?

  • The problem with languages is they don't compose. Iow, one missing feature needed automatically invalidates the language entirely. Swift is targeted at Apple platforms and cross platform is an after thought. Kotlin targets JVM and while it is cool in concept, I hate it as a user (and Kotlin native isn't near as mature). If were considering something else at this stage I'd probably put my time into F#, but even it has its cons.

    NOTE: By "compose" I mean that if I want feature A, B and C from lang X (has A and B), Y (has B and C) and Z (has A and C), but there is no way to get A, B and C in one language without creating a brand new one. I cannot mix and match features from different languages.

    • They don't compose what? Is this one of those 'monad is an endofunctor' deals?

      (me not knowing might be the ignorant bliss that allows me to just do productive things in them regardless)

      Edit: I understand from the updated comment

      I've long accepted that no one language can offer all of the language features I'd want, but I also question if I'd even want that language if I got it

    • And if we're still talking about gamedev, F# would probably make the GC too sad.

      (When are we getting a low latency collector in the CLR? But I digress...)

      1 reply →

Yeah I long for a language that has rust enums and pattern matching but none of the async or borrow checker.

Maybe I should just unsafe rust and see how I go....

I mean.. Java is there. Java 23 is really interesting.

  • When is Java getting value types? It has been talked about forever now.

    • And it will still take time to come, the whole engineering problem is how to introduce value types, make classes that are clearly value types like Optional, turn into value types, while at the same time not breaking the endless amount of JAR files in production, when upgrading to a JVM with value types support enabled.

      They would get it sooner by breaking the ecosystem, and as Python 3, Java 9, .NET Core have shown, not everyone would be racing to adopt the new shiny thingy.

      3 replies →

Rust is in a separate class of go and swift and kotlin and etc. The class it competes on is pretty C, C++, and itself.

Yes it's easier to write trivial code in python than Rust. Yes it's harder to manage memory manually than it is to let a gc handle it. I don't see the point.

Rust is a systems programming language. It's hard to write a server in it than it is to hack something in node, but it will also be faster and more reliable. Conversely, it's easier than writing it in C++ or C, while still being more reliable. That's the whole value proposition.

  • Swift has a systems programming subset, and an even smaller embedded programming subset.

    Of course "smaller" is significant here; it has less features and you might not like using it anymore.

  • For Apple, Swift is their Rust, regardless of the world outside Apple's ecosystem thinks about it.

    It is clearly stated on Swift's documentation, they already hold a couple of talks at C++ conferences about code migration, and is one of the reasons why nowadays they mostly focus on LLVM contributions instead of clang.