HNHacker News
TopNewBestAskShowJobs

norir

1,117 karma · joined November 17, 2022

For those who haven't been trained by it, no discipline feels better at the time, but painful.
submissionscomments
norir··on Let's get real about the one-person billion dollar company
You can argue it, but I disagree. Whatever their skills and experience, they did nothing alone.
norir··on Zig's Lovely Syntax
From my vantage point, Zig's syntax perfectly matches the language: it is ad-hoc, whimsical and serendipitous. It is lacking in grace, elegance and compassion.
norir··on Zig's Lovely Syntax
Significant whitespace is not difficult to add to a language and, for me, is vastly superior than what zig does both for strings and the unnecessary semicolon that zig imposes by _not_ using significant whitespace.

I would so much rather read and write:

    let x = """
      a
      multiline string
      example
    """
than

    let x =
      //a
      //multiline string
      //example
    ;
In this particular example, zig doesn't look that bad, but for longer strings, I find adding the // prefix onerous and makes moving strings around different contexts needlessly painful. Yes, I can automatically add them with vim commands, but I would just rather not have them at all. The trailing """ is also unnecessary in this case, but it is nice to have clear bookends. Zig by contrast lacks an opening bracket but requires a closing bracket, but the bracket it uses `;` is ambiguous in the language. If all I can see is the last line, I cannot tell that a string precedes it, whereas in my example, you can.

Here is a simple way to implement the former case: require tabs for indentation. Parse with recursive descent where the signature is

    (source: string, index: number, indent: number, env: comp_env) => ast
Multiline string parsing becomes a matter of bumping the indent parameter. Whenever the parser encounters a newline character, it checks the indentation and either skips it, or if is less than the current indentation requires a closing """ on the next line at a reduced indentation of one line.

This can be implemented in under 200 lines of pure lua with no standard library functions except string.byte and string.sub.

It is common to hear complaints about languages that have syntactically significant whitespace. I think a lot of the complaints are fair when the language does not have strict formatting rules: python and scala come to mind as examples that do badly with this. With scala, practically everyone ends up using scalafmt which slows down their build considerably because the language is way too permissive in what it allows. Yaml is another great example of significant whitespace done poorly because it is too permissive. When done strictly, I find that a language with significant whitespace will always be more compact and thus, in my opinion, more readable than one that does not use it.

I would never use zig directly because I do not like its syntax even if many people do. If I was mandated to use it, I would spend an afternoon writing a transpiler that would probably be 2-10x faster than the zig compiler for the same program so the overhead of avoiding their decisions I disagree with are negligible.

Of course from this perspective, zig offers me no value. There is nothing I can do with zig that I can't do with c so I'd prefer it as a target language. Most code does not need to be optimized, but for the small amount that does, transpiling to c gives me access to almost everything I need in llvm. If there is something I can't get from c out of llvm (which seems highly unlikely), I can transpile to llvm instead.

norir··on Show HN: Dlg – Zero-cost printf-style debugging for Go
The problem is calling anything zero cost is inherently wrong. Nothing in life has no cost. I know I am being pedantic but I think a more accurate description is "zero production runtime cost," which is also how I interpret rust's zero cost abstraction. In that case too, I find the terminology grating because there are very obviously costs to using rust and its abstractions.

One obvious cost to code that is removed in production is that there is now a divergence between what your source code says it does and what it actually does. I now need to keep in mind the context the program is running in. There is a cost to that. It might be worth it, but it isn't zero.

norir··on Futurehome smart hub owners must pay new $117 subscription or lose access
It is hard to overstate how wrong they are doing things if it costs more than a dollar or so per year for managed configuration. I previously worked for a cloud managed device company and it was obscene how high the margin was on the mandatory software licenses we bundled with the hardware and we were also collecting a huge amount of data, not just providing configuration.
norir··on Pony: An actor-model, capabilities-secure, high-performance programming language
I agree and disagree completely with this statement. Syntax is superficial. It is the first thing that people will notice about the language (unless you hide it from them). One quickly notices that if you don't like a language syntax, you can always write a compiler that operates at a purely syntactic level to transform your desired syntax to the actual target language.

But just because syntax is superficial doesn't mean that it isn't important. If a language has such poor syntax that I feel the need to write my own compiler to work around its syntax, I have to seriously question the skills and/or motivations of the author. If I am capable of writing a compiler at the syntactic level, why not just go all in and write my own compiler that implements _my_ desired semantics? A language that I find subjectively distasteful at the syntactic level is nearly guaranteed to be filled with semantic and architectural decisions that I also dislike. Consider Rust, I do not think that its syntax and abysmal compilation times can be decoupled. I would rather write my own borrow checker than subject myself to writing rust. And the reason is not the syntax, which I do strongly dislike, but the semantic properties of the language, such as horrible compilation times and compiler bugs (if a language has more than 100 open issues on github, I consider it broken beyond repair).

norir··on Why I write recursive descent parsers, despite their issues (2020)
There is a straightforward technique for writing unambiguous recursive descent parsers. The high level algorithm is this: parsing always consumes one character, never backtracks and is locally unambiguous.

You then construct the parser by combining unambiguous parsers from the bottom up. The result ends up unambiguous by construction.

This high level algorithm is much easier to implement without a global lexer. Global lexing can be a source of inadvertent ambiguity. Strings make this obvious. If instead, you lex in a context specific way, it is usually easy to efficiently eliminate ambiguities.

norir··on Why concatenative programming matters (2012)
This is mostly a problem of concatenative language design. There is nothing preventing function definitions from having type signatures that are statically checked. In my concatenative language compilers, I always provide a function that causes the compiler to halt and print out all of they types on the stack at the call site during compilation as well as a runtime function that prints out the runtime values. I have found this to be quite effective for debugging. Because I don't support global variables, these functions give a complete view of ALL of the live values at any point in the program, which is quite hard in most languages.

Local variables are also not necessary to have reasonable ergonomics, even in mathy contexts. Instead, the language can define special operators that access fields on the stack by type name and move or copy them to the top of the stack. For some reason, in the math example, the author deliberately added an unnecessary parameter that had to be dropped and put the arguments in the wrong order. Cleaning up the signature, there are several reasonable ways to solve the problem in a concatenative language with the features I've described.

    type n number
    type y n
    type x n
    def op_math_example [(y x) -> (n)] \\x * \\y \\y * + \y abs -
where \FOO moves a value of type FOO to the top of the stack and \\FOO copies the value to the top, leaving the original where it was. Yes, the \\ is a little ugly, but these should generally be used sparingly.

Even if we restore the original signature and use the author's syntax, there is a much cleaner solution to the problem without the move/copy operators.

    def sq dup *
    def op_math_full drop swap sq over sq + swap abs -
This is extremely cryptic. I suspect most forth programmers would have added a stack signature comment, but it is even better if the compiler statically verifies the signature for us, which is how the language I am describing differs from vanilla forth (where stack comments also have the risk of being wrong in an evolving system). If we restore the type system, it becomes:

   type n number
   type x n
   type y n
   type z n
   def sq [(n) -> (n)] dup *
   def op_math_impl [(y x) (n)] sq over sq + swap abs -
   def op_math_full [(x y z) -> (n)] drop swap op_math_impl
Of course it is still not as obvious to the reader what op_math_full actually does as (y * y) + (x * x) - abs(y). But in practice it would have some name like projection_size so once it has been written correctly, it doesn't really matter that it is a bit obscure to read at a glance. You also get really good at simulating the stack in your head and they type signature makes this much easier. Still, when you are confused, you can always add stack debugging like so:

    def projection_size [(x y z) -> (n)] drop swap PRINT_STACK sq over sq + swap abs -
    1 2 3 projection_size
and the output would be something like:

    ({ y 2 } { x 1 })
Writing programs in this style is very nice so long as you have enough tooling to facilitate it.

I have only used concatenative languages that I have implemented myself, however. I have read quite a bit about forth, but for me, there are clear problems with vanilla forth that are solved for me with the meta operators like [, [[, \ and \\ as well as good debug tools like PRINT_STACK above. Forth was an incredible discovery for its time. We can do a lot better now.

norir··on Covers as a way of learning music and code
> Sometimes, he'd run into things which didn't make sense. Why is this a doubly-linked list here, when it seems a singly-linked list would do just fine? And in those moments, if you can't find a reason? You get to go down that path, make it the singly-linked version, and then find out later: oh, ohhh. Ohhhh, they did that for a reason.

Everything is done for a reason. Unfortunately, often the reason is the author didn't know what they we were doing.

norir··on There is no memory safety without thread safety
It is very unfortunate that we use fixed width numbers by default in most programming languages and that common ops will silently overflow. Smarter compilers can work with richer numeric primitives and either automatically promote machine words to big numbers or throw an error on overflow.

People talk a lot about the productivity gains of ai, but fixing problems like this at the language level could have an even bigger impact on productivity, but are far less sensational. Think about how much productivity is lost due to obscure but detectable bugs like this one. I don't think rust is a good answer (it doesn't check overflow by default), but at least it points a little bit in the vaguely correct direction.

norir··on There is no memory safety without thread safety
The sad thing is that most languages with threads have a default of global variables and unrestricted shared memory access. This is the source of the vast majority of data corruption and races. Processes are generally a better concurrency model than threads, but they are unfortunately too heavyweight for many use cases. If we defaulted to message passing all required data to each thread (either by always copying or tracking ownership to elide unnecessary copying), most of these kinds of problems would go away.

In the meantime, we thankfully have agency and are free to choose not to use global variables and shared memory even if the platform offers them to us.

norir··on Reverse engineering GitHub Actions cache to make it fast
Here is my cynical take in ci. Firstly, testing is almost never valued by management which would rather close a deal on half finished promises than actually build a polished, reliable product (they can always scapegoat the eng team if things go wrong with the customer anyway).

So, to begin with, testing is rarely prioritized. But most developer orgs eventually realize that centralized testing is necessary or else everyone is stuck in permanent "works on my machine!" mode. When deciding to switch to automated ci, eng management is left with the build vs buy decision. Buy is very attractive for something that is not seriously valued anyway and that is often given away for free. There is also industry consensus pressure, which has converged on github (even though github is objectively bad on almost every metric besides popularity -- to be fair the other larger players are also generally bad on similar ways). This is when the lock in begins. What begins as a simple build file starts expanding outward. Well intentioned developers will want to do things idiomatically for the ci tool and will start putting logic in the ci tool's dsl. The more they do this, the more invested they become and the more costly switching becomes. The CI vendor is rarely incentivized to make things truly better once you are captive. Indeed, that would threaten their business model where they typically are going to sell you one of two things or both: support or cpu time. Given that business model, it is clear that they are incentivized to make their system as inefficient and difficult to use (particularly at scale) as possible while still retaining just enough customers to remain profitable.

The industry has convinced many people that it is too costly/inefficient to build your own test infrastructure even while burning countless man and cpu hours on the awful solutions presented by industry.

Companies like blacksmith are smart to address the clear shortcomings in the market though personally I find life too short to spend on github actions in any capacity.

norir··on Self-taught engineers often outperform (2024)
I actually do deny that university classes are great. Many are actively harmful. When I went to university, the intro level cs class was taught in c. It took me decades to unlearn.
norir··on Strategies for Fast Lexers
Lexing being the major performance bottleneck in a compiler is a great problem to have.
norir··on The jank programming language
Having scanned the source, I believe there is a better design for this language.

There exists a much smaller subset of jank that is capable of compiling itself. I am quite sure that you could write a bootstrap compiler in clojure (but restricted to the same subset) that translates to clojure (or some other language you prefer targeting). My guess, from experience writing small compilers, is it should be doable in ~5000 lines or even less.

Once you had that, you could then write a backend that translates to c. This gives you a native compiler for this small subset of jank. Applied to its own source code, you get a native compiler binary for free and cut the cord from clojure.

Now you have a native compiler and a very small, tight language. Because it is so small, it is easy both to check correctness and optimize. You can add new backends relatively easily. For the repl, you can compile functions into their own hash-addressed shared libraries using a c compiler and dlopen them. TCC is very fast for this purpose even if the generated code isn't quite as good as optimized gcc or clang.

All of the future vision could be implemented on top of this foundation. The current approach is likely going to just bog the author down, dealing with accidental complexity from c++ and llvm especially. It is also better to have fewer features implemented completely and correctly than more features incompletely or incorrectly implemented.

norir··on What is going on in Unix with errno's limited nature
I once worked on a project that had openssl as a dependency. We found that about 90 seconds in, the connection would drop a high percentage of the time. I laboriously worked through both the openssl and dependency code until finding that the bug was due to some code in the dependency not manually clearing out errno for openssl which caused a later read to hard fail when it should have soft failed with ewouldblock (or whatever).

Global errno is a bad design that is a consequence of bad design in c. If c supported tagged unions and pattern matching, there would be no way to access the result or error value without first checking what type of value it is. But since c doesn't support this, all these apis require programmer discipline that cannot be relied upon. As a consequence, it becomes almost inevitable that you encounter some inscrutable bug that someone else wrote due to their lack of skill and/or discipline. That doesn't make sense to me though I do understand why it is the way it is.

norir··on My first verified imperative program
My brain has been slowly trained to reject imperative programming. This example could be rewritten in a tail recursive manner using an immutable set which would be simpler to verify for correctness even without a formal verifier.

I have found that while there is a learning curve to programming using only recursion for looping, code quality does go significantly up under this restriction.

Here is why I personally think tail recursion is better than looping: with tail recursion, you are forced to explicitly reenter the loop. Right off the bat, this makes it difficult to inadvertently write an infinite loop. The early exit problem is also eliminated because you just return instead of making a recursive call. Moreover, using recursion generally forces you to name the function that loops which gives more documentation than a generic for construct. A halfway decent compiler can also easily detect tail recursion and rewrite it as a loop (and inline if the recursive function is only used in one place) so there need not to be any runtime performance cost of tail recursion instead of looping.

Unfortunately many languages do not support tail call optimization or nested function definitions and also have excessively wordy function definition syntax which makes loops more convenient to write in those languages. This conditions one to think in loops rather than tail recursion. Personally I think Lean would be better if it didn't give in and support imperative code and instead helped users learn how to think recursively instead.

norir··on Zig breaking change – Initial Writergate
It deeply bothers me that the zig team/community has been aggressively marketing this language while constantly making breaking changes without even providing migration tools. This is user hostile behavior. They are creating extra work for downstream users with minimal gain.

Let's take this change as an example. If I already wrote a program that used the old apis and meets my needs what is the benefit of this change for me? Now I have to go back and rewrite my old code and I might introduce a new bug in the migration, especially if I don't understand all of the nuance in the difference between the apis. Even if I agree that the new apis are better, the cost of migration might outweigh the benefits, but I am forced in to the migration or forking the compiler, which both might be bad choices for me.

It is not necessary to do this. They could, for example, have versioned stdlib and then maybe all I need to is pin my stdlib version. One complaint is that having multiple standard libraries causes more maintenance burden, but why should that be the primary concern? What is the cost to the maintainer vs. the cost to the community. If you make 1000 users spend an hour migrating their code, are you really going to save 1000 hours of maintenance burden?

Moreover, if the zig team wrote code with the assumption that they can never get rid of it, perhaps they wouldn't make so many design mistakes that all of these breaking changes become inevitable.

If I wrote a program in zig, I would feel obligated to also learn how to bootstrap it so that I wouldn't be subject to unwanted breaking changes. But then I see that bootstrapping involves some bizarre wasm hack instead of the tried and true bootstrapping approach of writing a simple first pass compiler in c (or some other language) and I am reminded again why I would never choose this language.

norir··on Republican governors oppose 10-year moratorium on state AI laws in GOP tax bill
I would love to see a reverse atlas shrugged where all the programmers just stopped working and we could see how much the executive class could do without them through the magic of ai. As it stands, I feel most workers are increasingly facilitating their own dispossession.
norir··on How I use my terminal
I don't even understand what point you are making. The essence of programming is tool making. Of course the best programmers make tools as do the worst. Are you seriously comparing making a terminal hot rod configuration app to, say, creating Tex?
norir··on Congestion pricing in Manhattan is a predictable success
Yeah, I imagine they are often projecting their own frustration over parking onto their customers. Every time a customer comes in and grumbles about parking, it triggers their confirmation bias. Conversely, new customers who only popped in because they were on foot are probably less likely to express that fact.

Given how annoying parking is, I'll bet that there are also many business owners who would trade some profit for their own ease of parking. Especially given that they have the power to squeeze their employees rather than bear the full cost themselves.

norir··on In praise of “normal” engineers
I tend to think that much of the difference between great and mediocre engineers comes down to mindset. The great engineers I've encountered have a commitment to making everything they touch better. They are adaptable and persevere. They believe they will either succeed at the task at hand or conclusively determine that it isn't possible given the current constraints. They recognize that failure will happen and do not get discouraged by it and instead see it as an opportunity to growth. When they encounter an issue caused by systemic problems, they will try to fix it systemically. They will not ship the first draft and will take a little it of extra time to get things right, which slows the initial release slightly but more than pays for itself with a dramatically lower maintenance burden.

This type of engineer is often misunderstood and underappreciated by management. Management is often motivated by immediate short term goals. Instead of cherishing the work of the engineers who build the foundational systems that will enable the long term success of the org, they complain about them missing arbitrary short term goals and accuse them of doing engineering for engineering sake instead of real work.

Management will celebrate the coder who whips up a buggy, but cool, feature in a week and will look the other way at the fact that the feature will always be a little bit broken because of its shoddy construction and instead will allocate some lesser engineers to maintain it. If instead the feature had been built correctly from the start, it may have been launched a bit later, but the overall cost will be much lower. Moreover, the poor engineers who are forced to maintain it (and let's be honest, the people who quickly churn out shoddy but shiny work almost never have to maintain it themselves) will not only be wasting their time, they will be actively internalizing the anti-patterns present in the code. This both inhibits their growth and understanding of good design principles and teaches them the bad lesson that crap is good and unless they have a uniquely strong character or good mentors, they will tend to either become demoralized (hurting their productivity and value to the company) or they will internalize the incentive to get out of maintenance work and build shoddy features of their own to pass down to the next poor soul.

The truly great engineer is the one who breaks this cycle by building extendable systems that are correct by design and takes accountability for everything they ship. They raise up the entire org both by their work and example. In the long run, they will be far more productive and positively impactful than the sloppy cowboy coder.

Unfortunately, the industry writ large celebrates and incentivizes cowboy coding so doing the right thing is very much against the grain. Indeed, the people who rise up the org chart tend to be the cowboys so they cannot even see the value of the other way and will often actively antagonize or undermine those who do the right thing (and threaten their dominant position in the org).

norir··on The Zed Debugger Is Here
Thankfully for me, I guess, neovim broke my config about a year ago by changing the default color scheme in a way that I could not fix easily with confug. So I forked and built my own and will likely never update it.
norir··on Guess I'm a Rationalist Now
One of my many problems with rationalism is that it generally fails to acknowledge it's fundamentally religious character while pronouncing itself superior to all other religions.
norir··on Andrej Karpathy: Software in the era of AI [video]
Have you thought through the downsides of letting go of these formal foundations that have nothing to do with job preservation? This comes across as a rather cynical interpretation of the motivations of those who have concerns.
norir··on I Wrote a Compiler
Writing a compiler without first learning how to write a parser is like learning to play an instrument without practicing scales.
norir··on The Grug Brained Developer (2022)
Here is my circular argument against debuggers: if I learn to use a debugger, I will spend much, possibly most, of my time debugging. I'd rather learn how to write useful programs that don't have bugs. Most people believe this is impossible.

The trouble of course is that there is always money to be made debugging. There is almost no incentive in industry to truly eliminate bugs and, indeed, I would argue that the incentives in the industry actively encourage bugs because they lead to lucrative support contracts and large dev teams that spend half their time chasing down bugs in a never-ending cycle. If a company actually shipped perfect software, how could they keep extracting more money from their customer?

norir··on The Grug Brained Developer (2022)
Perhaps consider that your experience is not universal and that others have good reasons for their decisions that are not simply ignorance.
norir··on The Grug Brained Developer (2022)
This reminds me why I abandoned scala. That being said, even a small sbt project can cold boot in under 10 seconds on a six year old laptop. I shudder to think of the bloat in play if 30 seconds is the norm for a hot reload.
norir··on The Grug Brained Developer (2022)
I understand you are likely doing this for shiny rocks, but life is too short to spend on abominable code bases like this.
← PreviousPage 3 of 15Next →