HNHacker News
TopNewBestAskShowJobs

humzashahid98

63 karma · joined April 28, 2024

submissionscomments
humzashahid98··on Show HN: Pong Wars in Standard ML
Raylib is definitely a pretty fun and simple library. I used it with OCaml (which has bindings to it already) before.

One thing I like about the Functional Core, Imperative Shell architecture is that, if you gather input into an immutable record before sending it to your update function, then your handling of input becomes independent of the windowing/control library you use.

That would make it easier to switch to SDL or GLFW if you later want to since you only need to rewrite a minimal amount of code which converts the windowing library's inputs to your own record type. (Although it's not easy to abstract over drawing operations that way.)

If you'd like any help, I'm happy for you to shoot me an email (my email is in the repository's LICENSE file). Although there's still a lot I could learn more about too.

humzashahid98··on Apple blocks PC emulator in iOS App Store and third-party app stores
Thank you for the insightful answer! That's nice to know. I hadn't considered that they had a system like that in place.
humzashahid98··on Apple blocks PC emulator in iOS App Store and third-party app stores
Is the "no JIT' policy somehow baked into the hardware/software of iOS devices, instead of something Apple finds by doing an app review?

I thought it was the latter (that running a JIT on iOS would be possible but not accepted on the app store), but then I'm left wondering why they seem to have submitted a JIT-less version on a third party app store.

Maybe the intent was ease development by having only one version to support for the first-party and also third-party app stores.

humzashahid98··on Hurl, the Exceptional Language
I prefer to build result-passing, no-exception-throwing systems out of an exception-throwing core where the language itself may throw exceptions but the thing I build from the language always returns result.

Elm is an extreme case where indexing into an array returns a result type instead of an exception when the index is out of bounds, unlike most languages including Haskell.

Maybe my program logic is intended to always access only valid indices in an array, but here I'm given a result type where I have nothing to do in the error case since my code is never intended to reach that case. I would rather let the language throw an out-of-bounds exception here to tell me that my implementation is incorrect while I'm testing.

Same with libraries in a language. It really depends on the use cases of the library you're writing whether results or exceptions are better. The most convenient thing for the user of a library would be to provide both exception-throwing and result-passing alternatives. This is what the OCaml standard library does as well.

humzashahid98··on Study finds that 52% of ChatGPT answers to programming questions are wrong
Stack Overflow is a community with an answer-rating system and there is often some level of review from other people commenting on the answer's advantages and shortcomings. You often have multiple answers to choose from too. Those features build trust in an answer or prompt you to look elsewhere.

The UI for an LLM answer would have difficulty replicating the same thing since every answer is (probably) a new one and you have no input from other people about this answer too.

Edit: After writing my reply, I saw that roughly four other people (so far) made the exact same point and posted it a couple of minutes before. I think your question is a good one (made me think a little) and apologies it feels like you're being piled on.

humzashahid98··on LunarML – Standard ML compiler that produces Lua/JavaScript
Standard ML is pretty simple and easy to pick up, but a weak point is the lack of { record with updatedValue } syntax to create a new record which is the same as an existing one but with some value changed. You have to specify every field in the record which can be a pain depending on how large your record is.

Standard ML has some nice features though and I think what I mentioned is the only serious shortcoming (except for ecosystem if that's important to you).

Standard ML is also structurally (but strongly) typed, unlike F# and the vast majority of nominally typed languages, which is rare but nice.

type person = { age : int, name : string }

is the same as the type

type animal = { age : int, name = string }

but this hasn't been an issue foe me, and structural typing means you can avoid defining the type at all which is convenient for small records.

For example, a function to increment age has no need for you to define the type.

fun grow { name, age } = { name = name, age = age + 1 }

I think Standard ML helps you understand the origin of some common FP idioms. Like tuples are really just records and SML treats them as such. The empty record {} is the same as the empty tuple (): both are the unit type. And a record like { 1 : "a", 2 : "b" } is exactly the same as the tuple ("a", "b").

humzashahid98··on LunarML – Standard ML compiler that produces Lua/JavaScript
This is a very cool project.

There is also MLKit's SMLtoJs which compiles to Javascript, but not to Lua.

https://github.com/melsman/mlkit/blob/master/README_SMLTOJS....

humzashahid98··on Counted B-Trees (2017)
B+ Trees (which store data at leaves) rather than B-Trees are what you want for good iteration performance usually.

An ordered B-Tree iteration would jump between different arrays in the tree often, but a B+ Tree would only have you jump to another array once you’re done iterating over all the elements in the current array.

The following article says the same. https://www.scylladb.com/2021/11/23/the-taming-of-the-b-tree....

humzashahid98··on NoTunes is a macOS application that will prevent Apple Music from launching
Thanks; I didn't see that debate on security and I can see it being a valid point, especially with the burden of a technically-inclined person having to fix others' (family/friends) devices.

I was trying to point to the opinion of the majority that I've seen and what I have seen from most of the community when security is mentioned is that Apple needs stronger security from a technical standpoint rather than controlling what and what is allowed on users' devices. I think both opinions hold validity, and you may be right that there is more nuance.

humzashahid98··on NoTunes is a macOS application that will prevent Apple Music from launching
I don't really have the same impression that you have had when it comes to discussions about Apple's approach to HN.

This linked comment (which I randomly found on HN's search) summarises what I've seen here:

"The way these discussions usually go is that a bunch of Apple users complain about the restrictions, a bunch of Apple users say they like them, and a few Android users like myself remind the complainers that there are, in fact, other options that don't involve forcing Apple to take away the rules that most iOS users appreciate having."

https://news.ycombinator.com/item?id=39574040

I do recall a few users opposing allowing alternative app stores (which is the debate that is freshest in my mind) because they prefer to stick to Apple's, and the common rejoinder from the community that nothing is being taken away from those who prefer to stick to Apple's store but that other users who want something different have more choice.

I'm still curious about what additional nuance you have in mind, since I honestly can't remember seeing it.

humzashahid98··on NoTunes is a macOS application that will prevent Apple Music from launching
Could you tell us what nuance you have in mind?
humzashahid98··on NoTunes is a macOS application that will prevent Apple Music from launching
This is plainly true considering how hard Apple fought to suppress alternatives to the "App Store". I don't know why your comment was downvoted.
humzashahid98··on Piece Chains (2006)
I appreciate the history section in particular.

VS Code is known to use a Piece Tree (Piece Table represented as a balanced binary tree) and I thought AbiWord (which this article also mentions) was the first to use the data structure. This article points out that the Tweak editor and the MText interface in the Modula-3 programming language uses the same approach, meaning this variant of the data structure goes back even further than I knew.

humzashahid98··on Translation of Rust's core and alloc crates to Coq for formal verification
I thought to myself a rename to FACT Prover (Formerly known As Coq Theorem Prover) would be nice when seeing they wanted to rename the project and it's a bit of a disappointment that didn't quite pan out.

Rocq is similar to the Roc programming language which is confusing naming (and they sound identical as far as I know).

humzashahid98··on The Cult of the Haskell Programmer
You might benefit from this video [1] on the "Functional Core, Imperative Shell" pattern in Ruby (a very object-oriented language), which sounds like one of the things you are having trouble with.

The talk is about segmenting IO and mutation from the rest of your program. The core of your program should be just pure functions and the IO part of your program should be "at the edge".

For example, consider a game loop. Game loops tend to have a while loop like this:

function loop() { while (should_not_close_window) { update(); // Run update function which updates mutable state draw(); // Draw from the mutable state } }

If we wanted to segment IO, we would return an immutable type/value from the update function instead like:

function loop(old_state) { const new_state = update(old_state); // Call a function which returns a new state given an old_state. draw(new_state); // Draw function still uses IO because drawing is inherently imperative

  if (new_state.should_not_close_window) {
    loop(new_state); // Call the loop function with the new state; this tail recursion replaces the previous while-loop). The program automatically exits the loop if new_state.should_not_close_window is false.
  }
}

The key thing is to turn as much of your program into a function that takes a value and returns a new value as possible, and let the IO code be only there for operations which cannot be done without IO (like drawing).

There are some cases where you might find yourself wanting to perform IO inside the update call but that can be handled by setting some state (inside your root state object) which the boundary of your program interprets as "do IO at the boundary".

For example, let's add an async handler to the loop (async often being IO like disk writes).

// Function to handle async messages function handleAsync(old_state) { for (const msg of old_state.msgs) { execMsg(msg); // Call function to pattern match and execute this specific message } const new_state = remove_msgs(old_state); // Call function that returns same state, except msgs is now the empty list (because the msgs have been handled) return new_state }

Where type msgs could be a list of types like | WriteFile of string | MaximiseScreen

The main loop will be modified by putting a call to this handleAsync function after update, like this:

function loop(old_state) { const mid_state = update(old_state); const new_state = handle_async(mid_state); draw(new_state);

  if (new_state.should_not_close_window) {
    loop(new_state);
  }
}

The majority of your code should be in the pure update() function which returns a new value (this example doesn't explain anything about pure functions at all) and IO should be minimised.

Generally, don't do IO (except at the boundary/root of your application). When you want to do IO, add some kind of message/datatype value to your state object and do the IO when your state object bubbles up, rreturning to the root of your application.

The talk likely explains this better but I hope my attempt at giving an explanation was helpful.

[1] https://www.destroyallsoftware.com/talks/boundaries

humzashahid98··on The most backdoor-looking bug I've ever seen (2021)
This made me think of that old TV show "Malcolm in the Middle".
humzashahid98··on Algebraic Data Types for C99
I think you're right to point out that it's too strong a claim to say that sum types are a basic building block of thought, although I believe they are very useful in coding regardless of that claim.

There is the still the ongoing debate about how much human perception and human reason are shaped by cultural forces vs. universal forces (where the latter asserts humans reason in the same/similar ways).

There's evidence that certain optical illusions don't work across cultures for example (I seem to remember those in Western countries have a tendency to mentally group things in rectangular boxes). The exact balance between cultural and universal forces isn't known and I doubt we could say anything about sum types in that regard.

humzashahid98··on Social engineering takeovers of open source projects
You're not alone. It made me laugh too.
humzashahid98··on Nintendo blitzes GitHub with over 8k emulator-related DMCA takedowns
If emulators in general were illegal, Nintendo would be in trouble too and would be obliged to remove the emulated games it sells on the Virtual Console.
humzashahid98··on Got an old Raspberry Pi spare? Try RISC OS. It is, something else
You're right. We also have proverbs like "two heads are better than one" and "standing on the shoulder of giants" so people recognise that both sides are important and have their value.

Right now, I'm working on my own on a personal project attempting to do something a little novel and I appreciate being able to go back and refine my ideas/previous code based on things I learn and additional thinking (even rewriting from scratch), when I'm more likely to face friction (like "stick to the suboptimal approach; it's not that bad") and cause trouble for teammates if I was working with someone else. So the value of working alone speaks more to me currently than the value of working in teams, but they both have their place.

humzashahid98··on Got an old Raspberry Pi spare? Try RISC OS. It is, something else
Related to this subject is Casey Muratori's video about Conway's law and a possible extension to it. The communication overhead of working in teams, and the fact it's harder to address cross-cutting concerns in them, is a key theme in it.

https://www.youtube.com/watch?v=5IUj1EZwpJY

There is also Descartes' quote about how a work produced by one master is often better than one in which many are involved, because of the unifying vision.

humzashahid98··on Uno: Create Beautiful Cross Platform .NET Apps Faster
Hi there. Appreciate the response!

I didn't know that the HTML DOM is being used for Uno's web output but that's good to hear. I was under the impression that things were being drawn to canvas (like how Flutter's web output works) but functionality like Ctrl-F (text search) works on https://gallery.platform.uno/, showing that's not the case.

I'm not sure why I had the impression that the web output used HTML canvas, but maybe that was something that changed since I last looked a few years ago or there might be another reason I was mistaken.

I think one misleading thing that gives the wrong impression is that text isn't selectable on Uno's web output (like https://gallery.platform.uno/), similar to how text isn't selectable on HTML canvas.

I think it's worth having a discussion about that with other employees, because it's one divergence from user expectations about how the web usually works (but application developers may also prefer to make text not selectable so it may be the behaviour you want in some cases after all).

humzashahid98··on Uno: Create Beautiful Cross Platform .NET Apps Faster
Uno wants applications to look the same across platforms (as Flutter does) but still wants to use native controls. So their approach[0] is to have default styling applied to every control which makes the native controls look the same on each device [0].

I think the main benefit with this approach is expected-behaviour (like how different desktop operating systems have different textbox behaviour), and that whatever accessibility you get by default with native controls is there. [1]

I don't really find their approach to GUI development compelling though, with them choosing a middle ground between "wrapper around native controls" and "implement everything yourself".

[0] Except on Linux and web, where Uno draws everything itself, imitating controls that look like GTK (on Linux) or UWP/WinUI (on web). These are the platforms I briefly tested on and I didn't have an enjoyable experience with the output due to non-native/non-expected behaviours.

[1] Page on accessibility: https://platform.uno/docs/articles/features/working-with-acc...

humzashahid98··on New startup sells coffee through SSH
vi-improved
humzashahid98··on Bytecode VMs in Surprising Places
The Java installer's installaation text "_ billion devices run Java" makes more sense after this. That's pretty amazing; thanks for the link.
humzashahid98··on Zed Decoded: Rope and SumTree
I have a simple (well, simple by my standards because I've spent a long time with it) implementation of a rope in Standard ML [0], OCaml [1] and F# [2]. (See tiny_rope.sml, brolib.fs or lib/tiny_rope.ml for implementation if you can read any of those languages; the F# implementation is probably easiest to read.)

[0] https://github.com/hummy123/brolib-sml

[1] https://github.com/hummy123/brolib

[2] https://github.com/hummy123/brolib-fs

The essence of a data structure is a binary tree where the internal nodes (the N2 case) contains a pointer to the left subtree, an integer containing the total length of the strings in the left subtree and a pointer to the right subtree. Then there are leaf nodes (the N0 case) which contain simply strings. (There are some other cases in the type like N1, N3 and L2, but those are solely for balancing because I built my ropes on top of 1-2 Brother Trees as described by Ralf Hinze, and those aren't essential to the rope data structure.)

When indexing (which is necessary for the insertion and deletion operations), you have a simple recursive algorithm which can be best seen in the recursive "ins" function. In the internal N2 nodes, the algorithm is to compare the index (given as an argument) with the left metadata. If the index argument is less than the left metadata, recurse to the left subtree passing the same index; otherwise, recurse to the right subtree, subtracting the index argument with the left metadata.

By the end, when you eventually reach the leaf case, the index argument is equal to the position you want to insert into in the current node. (I haven't tried to understand the maths behind this but it's how the data structure works.) At that point, all you do is insert into the leaf node's string (this is the same as inserting at an arbitrary index in any normal string) if you can without exceeding the maximum limit, or else you can insert another node.

Then you unroll the recursion. Unrolling the recursion involves updating the left subtree metadata when you reach the parent, and it also involves balancing. (I'm using 1-2 Brother Trees for balancing but ropes don't really care which balancing you use or if you use one at all.)

That's pretty much all there is to ropes. The deletion and substring algorithms just require minor modifications (the user might specify a range that includes more than one subtree, so you might need to recurse on both subtrees).

You can extend the idea behind ropes to hold more metadata too. For example, rope.sml (Standard ML) also tracks line metadata to allow indexing by line number. The changes required for this are: store an array at the leaf nodes containing indices of line breaks in the string also at this node, and at internal N2 nodes you should also store an additional integer indicating the number of lines in the left subtree.

There is an idea I haven't found too useful which is, if two leaf nodes have strings that can be joined without reaching the maximum limit, then join them. I haven't found this idea to improve performance much although it theoretically should.

I want to give a shout out to the MLton compiler for Standard ML here - the two rope implementations compiled with it handily beat the fastest ropes in Rust which is surprising. (My code performs well with F# and OCaml, but MLton takes it a level beyond that.)