Interesting perspective! I think HN would benefit if we shared our own mind-blowing insights here. A couple from the top of my head:
- Python: slicing, full reflection capabilities, and its use as an interface to almost anything [1], not to mention its role as an embedded interpreter (beyond the GIL debate).
- Z3: one of the closest I know for defining the ‘what’ rather than the ‘how.’ I got lost with Prolog when I tried to add more complex logic with ‘hows’, though I was a complete newbie with Prolog. Z3, however, really boosted my beginner capabilities.
- OMeta: allows me to write fast parsers without spending time resolving ambiguities, unlike ANTLR and other popular parsers using classical techniques.
- Smalltalk/Squeak: everything can be re-crafted in real-time, a blend of an OS and a programming language. The community vibe is unique, and I even have friends who implemented an operating system using Squeak. The best example? TCP/IP implemented in Smalltalk/Squeak! [3]. David Weil and I also created an early proof of concept [4] that used Linux behind the scenes, aiming to ‘compete’ with the QNX floppy disk.
- AutoLISP: a programming language embedded in AutoCAD, which I discovered in high school in the late ’80s—only to just find out that its first stable release is considered to be around 1995 [5].
- REXX on the Commodore Amiga: not only could applications be extended with this language, but they could also interact with each other. A decade later, when I used REXX on an IBM Mainframe, I still preferred the Amiga’s approach.
- Racket: I can draw in the REPL. For example with Racket Turtle.
- C++: object orientation "for C". I was not mesmerized by templates.
- Clipper: first database usage and relatively simple to create business apps.
- Objective-C: the first implementation of GCD [1], providing a C/C++ style language with fast performance, a clean event-loop solution, and reflective capabilities. However, I never liked the memory handling.
- Machine Code/Assembler on the Apple IIe: I could jump directly into the assembler monitor from BASIC.
- APL: a powerful yet complex way to program using concise expressions. Not ideal for casual use—best suited if you really understand the underlying concepts.
By the way, it’s time for Apple to embrace programming languages on iOS and iPadOS!
> ... sounds like my experience with Instaparse (Clojure)
Thank you! I wasn’t aware of Instaparse or its use of PEGs [1] which gives you the same sense about parsing ambiguities.
> REXX - I thought this was ingenious specifically for file/text processing
Formally the REXX in Amiga was called ARexx and included extensions [2]. REXX [3] itself is not specifically for file/text processing but enables you to connect different environments [4].
- HyperCARD (also on that list, and agree with almost all points made): It was magic to get into Developer mode and to create stacks --- it's unfortunate that Asymetrix Toolbook didn't get further, nor that the Heizer stack for converting HyperCard stacks to Toolbook wasn't more widely available, and a shame that Runtime Revolution which became Livecode reneged on their opensource effort --- hopefully someone will make good on that: https://openxtalk.org/
Unfortunately, I never got anywhere w/ Interfacebuilder.app or Objective-C....
- PythonSCAD: variables and file I/O for OpenSCAD (though to be fair, RapCAD had the latter, it was just hard to use it w/o traditional variables) https://pythonscad.org/
Go really blew me away with its explicit error handling. As someone who has come from the OOP cult of clean code and other useless principles that haven’t led our industry to have less messes over the past 20-30 years. Who was slowly moving into the “simplicity”, “build things that can be easily deleted”, “YAGNI” mindset it simply clicked.
Then Rust took it a few levels beyond Go.
It’s a really good list, I suspect the languages you add is going to spend on your experience. So I wouldn’t feel too sad if your favorite language is on that list. The author lists Java as a language with an amazing standard library, but something tells me a lot of people will have C#, Go or similar as their Java, and that’s fine!
I'm still convinced that Rust does it _right_ by allowing you to "chain through" errors using `Result`. Having to pepper `if err != nil { return ..., err}` repeatedly into your code is just distracting from the core logic - 99% of the time, that's the response anyway, so being able to just write your happy-case logic and have an implicit "but if there are any errors, return them immediately" (but still returned as a value rather than thrown as an orthogonal Exception) is the best of both worlds.
It is really great. I just wish there was a better story for combining error types. thiserror works, but it’s annoying boilerplate to have to write imo.
When you require that your "algebraic" types always be tagged, so that `type X = A + B` and `type Y = A + B` are always different, you lose most of your capacity of seamlessly composing the types.
In other words, Rust doesn't have a type that means "either IO Error or Network Error", so it can't just compose those two behind the scenes into something that you can later decompose. You have to create some tag, and tell the compiler how to apply the tag, because each tag is different.
That’s a good way to put it. Lots about Rust is great, but the lack of anonymous union types over complicates a lot of problems. The trait system would be much less of a headache if you could define a local child types as well.
The anyhow crate complements thiserror pretty well in my experience. I use it "top-down" facing where individual errors in any component are defined with thiserror, but then we bubble them up by wrapping them in a `anyhow::Error` if we don't know what to do with them. It also has the nice thing of being able to produce simplified stack traces to help diagnose where things are going wrong. And then you can downcast to component-level thiserror Errors if you want to inspect something closely from high up.
I have been using Golang recently and have the exact opposite feeling about it, which is likely due to my limited exposure to it. Needing to constantly check if a specific return value is not nil just seems so sloppy but I have yet to come across a "better" way as it appears to just be a Golang convention. Is there a good post about better ways to handle errors in Go?
This is the better way as far as I’m concerned. Go has a philosophy to be simple, and this is explicit error handling at its simplest. You deal with errors exactly where they occur. I think a lot of people simply prefer implicit error handling and long exception chains. There is nothing wrong with that but that’s not how Go does error handling.
After a couple of decades in the industry I really prefer explicit error handling because while powerful implicit error handling is also easy to get wrong. Which means I have to go through ridiculous chains of exceptions to find some error someone introduced years ago, instead of being able of heading directly to it and immediately understanding what is going on. So I guess it’s a little contradictory that I prefer the Rust approach, but I think the compromise of added complexity is small enough to make up for the added safety and “manoeuvrability”.
The flip-side is of course that Go’s simplicity is part of the reason it’s seeing a lot of adoption while most other “new” languages aren’t. (Depending on where you live).
I prefer implicit error handling if exceptions are checked and defined as part of the contract, otherwise it’s definitely hard to trace.
I know the biggest complaint with checked exceptions is that people tend to just use catch all exceptions, but that’s just sloppy work. If they’re a junior, that’s when you teach them. Otherwise, people who do sloppy work are just sloppy everywhere anyway.
My issue with a lot of the best practice principles in SWE is that they were written for a perfect world. Even the best developers are going to do sloppy work on a Thursday afternoon after a day of shitty meetings during a week of almost no sleep because their babies were crying. Then there are there times when people have to cut corners because the business demands it. A million little things like that, which eventually leads to code bases which people like Uncle Bob will tell you were made by people who “misunderstood” the principles.
Simplicity works because it’s made for the real world. As I said I personally think Rust did it better, but if you asked me to come help a company fix a codebase I’d rather do it for a Go one than Rust.
Truthfully, I disagree. I’ve worked at a few different companies and I could absolutely rank them in the quality of their staff.
Been on teams where every individual was free to use their best judgement, we didn’t have a lot of documented processes, and… nothing ever went wrong. Everyone knew sloppy work would come back and bite, so they just didn’t ever do it. Deadlines were rarely a problem because everyone knew that you had to estimate in some extra time when presenting to stakeholders. And the team knew when to push back.
On the other hand, I’ve been on teams where you felt compelled to define every process with excruciating detail and yet experienced people somehow screwed up regularly. We didn’t even have hard deadlines so there was no excuse. The difference between implicit and explicit error handling would have not mattered.
At the end of the day, some of these teams got more done with far fewer failures.
Almost all error-related code in Go is just doing by hand what an exception system would do automatically: stop the current function and propagate it to the caller. The error is not being "dealt with" in a meaningful way except at the outermost layers of the onion (i.e. HTTP/RPC middleware converting it to a wire protocol error response) which could have used a try/catch anyway.
Go has a philosophy to be simple, and this is explicit error handling at its simplest. You deal with errors exactly where they occur.
“if err != nil {return nil, err}” is the opposite of this philosophy. If you find yourself passing err up the call stack most of the times and most calls may return err, it’s still exception-driven code but without ergonomics.
It’s not simplicity, it’s head butt deep in the sand.
There is no other way - most error conditions can't be dealt with at the place where the error happened. A parseInt failing is meaningless at the local scope, the error handling depends on who is the caller.
Golang needs monads. If by default it had a monad that propagated errors up the stack, then it would be much cleaner to work with, and should you need another behaviour you could install a different monad.
The general idea is that you should pay the cost of handling an error right there where you receive one, even if you're just going to return it. This reduces the incremental cost of actually doing something about it.
If you're given an easy way out, a simpler way of just not handling errors, you either won't even consider handling them, or you'll actively avoid any and all error handling, for fear of polluting your happy path.
I can't say I agree with the approach all the time, but I know I'm much more likely to consider the error flow doing it the Go way, even if I'm comstant annoyed by it.
> If you're given an easy way out, a simpler way of just not handling errors
Doesn't go offer the simplest way of all to "just not handle errors"? Just ignore them. It is an option. You can ignore them on purpose, you can ignore them by mistake, but you can always simply ignore them.
But ignoring by mistake gets caught by linters, in practice.
And then doing it on purpose is almost as noisy as bubbling it, and sure to raise an eyebrow in code review.
My experience is with exceptions in Java/C#, Go errors, and C++ absl::StatusOr. In theory, I'd favor checked exceptions. In practice, I find that I'm always running away from polluting the happy path and coming up with contrived flow when I want to handle/decorate/whatever some errors but not others, and that giving types to checked exceptions becomes a class hierarchy thing that I've also grown to dislike. Both Go and C++/Abseil are indifferent if noisy to me (and C++ annoys me for plenty other reasons). Maybe elsewhere I'd find options better. Maybe.
Isn’t the reason by chance how try-catch works lexically? I find that as another overlooked billion dollar mistake. It’s not that a happy path gets a crack in it, but the absolute monstrosity of a catch ceremony together with a scope torn between two blocks. “try {} catch {} finally {}” should be “[try] { catch {} finally {} }” absolutely everywhere.
In practice I want the error propagated to the caller 95% of the time and swallowed 5% of the time. The problem is Go makes me explicitly spell out (and write a unit test case for!) the behavior I almost always want, while the rarely-desired behavior is default.
Error returning is explicit, error returning is declared in the function signature. Error returning plays nice with defer (there is errdefer) and there is limited sugar (try keyword) that makes life easier.
I personally like it better than exceptions (even if it's much noisier for the 95% common case as another poster put it), both of which I've used enough to appreciate the pros/cons of. But that's about it.
> Needing to constantly check if a specific return value is not nil just seems so sloppy but I have yet to come across a "better" way as it appears to just be a Golang convention. Is there a good post about better ways to handle errors in Go?
As others have identified, not in the Go language. In other languages which support disjoint unions with "right-biased" operations, such as Haskell's Either[0], Scala's version[1], amongst others, having to explicitly check for error conditions is not required when an Either (or equivalent) is used.
He basically simulates jump to catch by hand. I understand the authority of Rob Pike, but don’t understand why this idea is great.
Processes IRL don’t look like write() three times. They look foo(); bar(); baz(); quux();, which weren’t designed to cooperate under a single context. If only there was an implicit err-ref argument which could be filled by some “throw” keyword and the rest would auto-skip to the “catch”.
Coming from Python, as an amateur dev, error handling in Go was annoying as hell. But I had no other choice so I went for it.
Afer a few programs I realized I never really thought about error handling - Go forces you to decide where you want to handle your error, where to inform about it etc.
It is still annoying (especially the boilerplate) but it had a net positive value for me in terms of learning.
Very much agree with Erlang/Elixir. After years battling and hating the JVM, I was initially very put off by the existence of a VM (BEAM) and by how it forced the concurrency model on me, and it did make getting into serious development with it harder, but once I "got" it, it really blew my mind. The "let it fail" philosophy was particularly mindblowing to me (especially once I understood that it doesn't mean "don't do error handling").
Indeed. OTOH, "focus your main code on the happy path, and put your error handling in the supervisor tree" is unfortunately a bit less pithy.
Shades of "boring technology"[0] which might better be described as "choose technology that is a good fit for the problem, proven, reliable and proportionate. Spend your innovation tokens sparingly and wisely".
I'm personally caught between my attachment to the "boring technology" philosophy and my desire to try Elixir, which seems like exactly the kind of obscure and exotic technology that it rejects.
Erlang is old, reliable tech that used to scare people once upon a very distant time for
- running on a VM (now boringly normal)
- using a pure-functional language with immutable terms (the pure-functional language fad has since then come and gone and this is now archaic and passé if anything)
But languages only get stereotyped once. At any rate, it's pretty boring and old.
I actually think elixir is the perfect boring technology now. The language is matured, and is not changing much. There will be a big addition at some point with the new type system, but other than that the language is pretty much done. Underneath elixir is erlang, which is extremely mature and highly boring in the good way. Live view might be the only exception, but even that is now becoming boring as it has gotten stable. If you are a believer in boring, you should definitely give elixir a try
Already said by two sibling comments in all the detail I would have, but I wanted third it: as a subscriber to boring technology myself and actually never been one to jump on new shiny tech, Elixir/Erlang is the epitome of "boring technology." Elixir does a lot on its own you can often cut out the need for a lot of other dependencies.
Joe Armstrong distinguishes in his thesis between exceptions and errors: exceptions are when the runtime system doesn't know what to do, and errors are when the programmer doesn't know what to do.
Say your code divides by 0; the runtime system can't handle 1/0 but you the programmer may have anticipated this and know what to do. This is just someplace you write code right there to handle the case (catch an exception, pattern match the 0 case beforehand, whatever).
Your error, on the other hand, means something you expected to hold didn't; recovery inline from unknown unknowns is a fool's errand. Instead you give up and die, and the problem is now the exception you know how to handle "my child process died".
The distinction makes me think of Java's checked exceptions, which I think have gotten an unfairly bad reputation from a lot of slapdash programmers complaining that they didn't want to think about error-cases. (And, TBF, a little syntactic sugar could have gone a long way.)
In brief, a checked exception is part of a method signature, "function wash throws NoSoapException", and the compiler enforces that whoever writes code calling that signature must make some kind of overt decision about what they want to do when that exception comes up. That may mean recovery, wrapping it inside another exception more-suitable to their own module's level of abstraction, deliberately ignore/log it, or just throw a non-checked exception.
So in a sense checked exceptions suit those "error" cases where you do expect the programmer consuming your function to at least decide whether they can handle it, and if they don't you still get all those wonderful features like stack traces and caused-by chaining. In contrast, regular (unchecked) exceptions have the opposite expectation, that the caller probably won't be able to handle them anyway and must opt-in to capture them.
Round about the seventh language, he stops using exclamation marks. The list goes from "Mind blown: Programming my own games!" to "Mind blown: Some support for writing my own gradual type system."
I think that actually happens to us all. The first time you write an infinite loop to print out that your friend is a doofus to show off is magical. 25 years in and you start to value… other things… that maybe don’t have the childish whimsy and fun.
That’s OK. In the same way that when you start to read books, you might like Blyton, Dahl or diving into a Hardy Boys, by the time you’ve been reading fiction for a few decades your tastes might become a bit more philosophical.
The trick is to introduce these things to people when they’re ready for them, and to not treat those who haven’t experienced them as in some way inferior.
I’m not going to shove Proust down someone’s neck or make them feel dumb for not having read any of his work, in the same way I am going to think carefully about whether somebody would benefit from seeing some Prolog.
What does interest me a lot about this list is that it’s not just a “well, look, I found Clojure and think it’s better than Python, YMMV”, or “if you don’t like or understand Haskell maybe you are just too dumb to grok eigenvectors” brag piece.
There is a thought about what you might get from that language that makes it worth exploring. As a result I might revisit OCaml after a brief and shallow flirtation some years ago, and go and take a look at Coq which I doubt I can use professionally but sounds fascinating to explore.
Are you being flippant, or is there really a sense in which eigenvectors are relevant to Haskell? To the best of my knowledge despite all the category-theoretical goodness in the language, there's no meaningful language-level connection to linear algebra, -XLinearTypes notwithstanding.
I think that are a couple of programming langs out there that everyone needs to be in contact with at least one time. I will mention the three ones that actually changed my mind (I have taken these in a semester at uni): Haskell, Prolog and Smalltalk (even if you do not like nor use them, they will force you to think radically different). Then you should add a structured language (C or Pascal will suffice). If you like metal, try assembly. Yes there are others like APL and Forth but I can't say anything about those as I haven't tried them. Lastly a LISP or scheme flavour won't be bad to experience as well.
It was about APL that Alan J. Perlis said "A language that doesn't affect the way you think about programming, is not worth knowing." (number 19 of https://cpsc.yale.edu/epigrams-programming)
Learned programming with Pascal, and Turbo Pascal (not the language itself) was what blew my mind back then. The Swag libraries weren't the language itself neither, but for pre-internet age they were a jump forward.
Then had to do things in OS/2, and then it was the REXX turn. Shell scripting didn't had to be as basic as I knew from DOS. Some years later moved to bash, and from complex script to long but very powerful oneliners, so it was another shock.
Still was working with OS/2 when learned Perl, and regexes, and its hashes, and all with some interesting semantic approach on that. And for many years it was my "complex" shell scripting language. Python was not as mindblowing or at least had the same kind of impact on me.
I think he means that the stack is not something that you are forced to work with when programming in assembly. You can put data wherever you want (and are allowed to), and jmp into whatever random memory address you want. You can use CPU instructions that handle stack management for you, but you don’t _have_ to.
Somewhere on my random ideas pile is to write a queue-oriented operating system - you know how we have threads? What if we didn't have threads, just a list of things to do, to run on the next available processor? (Haskell's VM calls them sparks)
I mean, that's just a register. The memory region itself is not special in any way, a random "heap" region that would be similarly frequently accessed would be just as fast.
Except of course how return addresses are typically always stored on the machine stack. It is very concretely there and part of the programming model, in my world.
Technically some CPUs have a "jump and link" that saves the return address in a register. Then it is up to the calling convention to save that register on the, also by convention, stack.
I’ll second Prolog as mind blowing. It is a fundamentally different way to think about problem solving using a computer. Well worth even a trivial playing with it.
I've been doing advent of code with Uiua [0], an array and stack programming language. And it feels like playing Tower of Hanoi with data and building pipelines with functions.
Unfortunately Dijkstra and Iverson took personal dislikes to each other's approach, or we might have had a language that abstracted data flow like Iverson's APL and abstracted code flow like Dijkstra's Guarded Commands.
More no-code than programming language but Lotus Notes totally blew my mind.
Imagine going from pre World Wide Web - and for many companies pre-email - age of local area networks (for file sharing only) and 38.K modems directly to group and user oriented, email integrated, distributed, replicated, graphics and multimedia integrated, no-code, document oriented, user programmable, fully GUI, secure, distributed database applications.
Lotus Notes blew my mind completely. A truly amazing application at the time.
I wrote a lot of code for lotus notes circa 1995 for an early web app. It was still code (lotus notes script), just that I had to figure out how to do things without loops and recursion.
There were a few times I had my mind blown. Once with Scheme when I was watching the old SICP video lectures. But also (and this may be a rather unpopular thing to say) once with C when I had coded something up and compiled it and ran it and it finished instantly. Rust didn't really blow my mind, but I have taken some of its lessons (using error values, lifetime and ownership management) to heart.
Common Lisp, which at the moment is the only language I can see myself using unless I decide to make one myself, never really blew my mind, although the little things it does make it hard for me to imagine switching away from it.
Regular expression is not listed in the article, but it blew my mind when I first used it in Perl. Any of sed/awk/perl/python/ruby/javascript could have expose the author to regular expressions, but maybe they didn't find it as transformative as I did.
'' is one of the first special thing that computers made me look at. Even a simple msdos glob .exe felt like a superpower. regexps were the next level but a two edged weapon as everybody knows. That said I never leveraged PCRE extended capabilities.
> Just that the concepts either didn’t resonate with me or, more often than not, that I had already discovered these same concepts with other languages, just by the luck of the order in which I learnt languages.
I find this style changes the way I think about and write code transformations.
It's also in shell pipelines, R's magrittr, and Clojure's thread macros, and can be emulated in some OO languages with methods that return the transformed object itself.
I wish there was a linter that could limit it though. It's like coders' subconsciouses say "the feature exists, so I have to use it to the greatest extent possible". Or, "assigning intermediate values is now an anti-pattern", and we end up with things that are like 20 straight pipes, with more pipes inside lambdas of those pipes, etc., that result in a function that is totally incomprehensible. All it really saves us from is this:
let x1 = one(x);
let x2 = two(x1);
let x3 = three(x2);
The other advantage of being explicit is you can set breakpoints and see the intermediate values when debugging.
Granted, the same could be said about `three(two(one(X)))`, so it's not specifically a pipe operator problem. It's just that I see it a lot more ever since pipe operators (or their cousins "streams", "extension methods", etc) have been popularized.
My guess is it's because `three(two(one(X)))` across multiple lines would need indentation on each nested function, and end up looking obviously obtuse with too many levels of nesting. Pipes make it look cleaner, which is great at some level, but also more tempting to get caught up in "expression golf" leading to extremely concise but incomprehensible code.
I would second Prolog as mind-blowing. I've found you're typically confronted with fully engaging with the core of whatever problem you're solving, and only that core. This is probably what can make it so frustrating sometimes as you have no option but to work out the hard stuff nearly immediately; not to mention that unconsidered edge cases, mistakes can cause some pretty opaquely wrong results, or the query not terminating, which can make conventional debugging pretty difficult. The guarantees you get with using the 'pure' core of Prolog do open up some really interesting avenues though, for example Scryer's debugging library is quite neat in affording _semantic_ debugging: https://www.scryer.pl/debug
Just some additional commentary too - I think this post quite misrepresents it with some of the comparisons.
Prolog at its core is SLD Resolution [1] (a form of search) over Horn Clauses [2] (first order logic). Queries posted to the runtime are attempts to find a set of values which will satisfy (cause to be true) the query – whilst SQL is founded on relational algebra which more closely aligned with set theory.
Whilst there's probably some isomorphism between satisfying/refuting a logical predicate, and performing various set operations, I'd say it's a bit of a confusion of ideas to say that SQL is based on 'a subset of Prolog'. The author might be thinking about Datalog [3], which is indeed a syntactic subset of Prolog.
Same Prolog, and particularly Lambda Prolog literally blew my mind. I could (physically) feel my brain, trying to think differently about the problem to solve. It was an experience.
I need to retake that course (was 20 years ago...). I also wonder if/how AI could leverage lambda prolog to prove things.
I remember learning Prolog, it was tricky to wrap my mind around it, it wasn’t like any other language. The day I finally “got it” I was very happy, until I realized all the other languages I had previously learned, no longer made any sense.
I remember taking a PL class in undergrad, learning Prolog as one of a handful of languages. During that section my brain started to want to "bind" variables to things as I was going about my day, it was very weird.
While i am not too familiar with Prolog, i have been intending to study it in depth given that it can be used to design Languages/DSLs/Expert Systems/ML.
Here are some resources which i have been collecting;
4) User "bytebach" gives an example of using Prolog as an intermediate DSL in the prompt to an LLM so as to transform English declarative -> Imperative code - https://news.ycombinator.com/item?id=41549823
You can run pure ISO Prolog interactively in your browser on [1]. There's also an extended tutorial for solving toy container logistics problems there ([2]). Though while it doesn't require prior Prolog knowledge, it's not so much a learning resource as it is a teaser and practical guide for using Prolog's built-in combinatorical search for linear planning in the classic STRIPS style (David Warren's WARPLAN being among the earliest Prolog applications apart from NLP).
Not the OP but personally I would recommend looking into constraint programming which is related but different to logic programming (Prolog has extensions for Constraint Logic Programming which combines the two).
You mentioned you're looking for something new that doesn't have to be related to data analytics, well constraint programming (and similar) is basically the mirror problem. Instead of data you have the rules of the "game" and the solver's job can be to: find a solution, find the optimal solution, find all solutions, or prove there is no solution.
Things like scheduling problems, resource allocation problems, etc. A real-world example would be finding the most efficient cell placement when developing a microchip, this is basically an advanced rectangle packing puzzle.
Much like prolog you define the rules (constraints) and the solver takes over from there. Part of the fun is figuring out the most effective way to model a real-world problem in this manner.
The closest thing to Prolog in this domain would be ASP, with Clingo/Clasp being the best solver available. But you also have regular constraint programming (look into MiniZinc or Google's OR-Tools), mixed-integer programming which is mainly for hardcore optimization problems (commercial solvers sell for tens of thousands of dollars), satisfiability modulo theories (often used for software verification and mathematical proofs), etc.
The mind-blowing bit is that this sort of problem is NP-complete but these solvers can find solutions to utterly massive problems (millions of variables and constraints) in milliseconds sometimes.
Also look into ASP for the real mind blowing one. Specially clingo/clasp.
Much more powerful than Datalog, can even solve optimization problems without being turing complete.
The frustrating thing about prolog is that at first it seems you get to ignore the execution model, but very very quickly you have to learn to think of it in imperative terms anyhow, so you can ensure termination and stuff. Or at least, I never got far enough past that to wrap back around to not coding in it with a background imperative model running in my head.
* C++: The feeling that I can make a-n-y-t-h-i-n-g. (not true but still mostly true)
* Ruby: elegant, OO, who-needs-compile-time-if-you-have-unit-tests
* Haskell: hard to learn but so worth it, by doing so I became a follow-the-types person
* Elm: not hard at all, a well picked subset of Haskell for browser apps only
* Kotlin: a well typed Ruby, with the ecosystem of the JVM at your disposal
* Rust: when every bit of runtime performance counts (but now without sacrificing safety like with C/C++)
BASIC, but specifically on a TRS-80. I can just type something in and run it. I don't have to wait a week for the teacher to take the card deck over to wherever the mainframe is.
Pascal. That's it, all of it, in those ~4 pages of railroad diagrams expressing the BNF.
C. Like Pascal, but terser and with the training wheels removed. It just fits the way my mind works.
Java. The language itself is kind of "meh", but it has a library for everything.
I found Coq in the list to be very interesting. Is anyone using Coq here for serious math or programming? What kind of problems are you solving with Coq?
And I'd also like to know how different Coq and Lean are. I'm not a mathematician. I'm just a software developer. Is there a good reason for me to pick one over the other?
Coq is cool partially because it can be compiled to OCaml. So you can write part of your program in Coq and prove its correctness, and then write the rest in pleasant OCaml. I believe there are some popular ocaml libraries, like bigint implementations, that do this.
Lean is designed to be a general purpose programming language that is also useful for mathematical proofs. From that perspective it’s kind of trying to be “better haskell”. (Although most of the focus is definitely on the math side and in practice it probably isn’t actually a better haskell yet for ecosystem reasons.)
If you try either, you’ll likely be playing with a programming paradigm very different than anything you’ve used before: tactic oriented programming. It’s very cool and perfect for math, although I don’t think I’d want to use it for everyday tasks.
You won’t go wrong with either, but my recommendation is to try lean by googling “the natural number game”. It’s a cool online game that teaches you lean and the basic of proofs.
By the way, don’t be scared of dependent types. They are very simple. Dependent types just mean that the type of the second element of a tuple can depend on the value of the first, and the return type of a function can depend on the value passed into it. Dependent types are commonly framed as “types that have values in them” or something, which is a misleading simplification.
(zx spectrum) BASIC: nothing like the first contact with AI
C++: pointers, OOP and so much more - good and bad - all in one package
Fortran90: vector programming for humans
Python: general programming for humans
Biggest disappointment: Scratch. Why isn't visual programming more mind blowing?
Anyway, always looking out for these well-regarded "mind-blowing" languages but somehow they never show up in really mind-blowing projects? Would be interesting in this respect to have a list of mind blowing projects that were only possible due to a mind-blowing language.
I was waiting for this one. Julia's multiple dispatch and the amazing composability was eye opening. The first time you see exchanging a Measurement for a simple number and returning a computation with uncertainties. Or a Unitful for a simple number and returning a computation with units. Amazing!
I use it at work. I must say I've never experienced that as an issue, so perhaps we use the programming language in a different way.
One issue that i have experienced, which may be related, is that it's hard to have a little bit of controlled type dynamism. That is, once you have a type that is a union with many possibilities, you need to be vigilant not to do operations on it that makes type inference give up and return `Any`. This still bites me. There are some solutions to that in pacakges (e.g. the relatively new Moshi.jl), but I do miss it with language-level support.
Thanks a lot, I think that's what people meant by being surprised by functions over complex types. But it seems you can still be productive even without it.
Red/Rebol has a different, powerful approach to homoiconicity and DSLs.
And there is this XL language that has very interesting approach to extending the language, but sadly the compiler is not in a runnable state, I could only assess it by the docs.
Early on when I was learning, assembly language blew my mind. I went from Basic to Pascal but I wanted to make games so I tried my hand at C/ASM and it was just so wild to be that close to the metal after a couple years at such a high level.
In recent years, Go blew my mind with it's simplicity and how strong of a case it makes for writing boring code.
It's been decades since writing C/ASM and my guess is what little I remember isn't applicable anymore, but I plan on diving in again at some point if only to better understand Go.
212 comments
[ 5.2 ms ] story [ 442 ms ] thread- Python: slicing, full reflection capabilities, and its use as an interface to almost anything [1], not to mention its role as an embedded interpreter (beyond the GIL debate).
- Z3: one of the closest I know for defining the ‘what’ rather than the ‘how.’ I got lost with Prolog when I tried to add more complex logic with ‘hows’, though I was a complete newbie with Prolog. Z3, however, really boosted my beginner capabilities.
- OMeta: allows me to write fast parsers without spending time resolving ambiguities, unlike ANTLR and other popular parsers using classical techniques.
- Smalltalk/Squeak: everything can be re-crafted in real-time, a blend of an OS and a programming language. The community vibe is unique, and I even have friends who implemented an operating system using Squeak. The best example? TCP/IP implemented in Smalltalk/Squeak! [3]. David Weil and I also created an early proof of concept [4] that used Linux behind the scenes, aiming to ‘compete’ with the QNX floppy disk.
- AutoLISP: a programming language embedded in AutoCAD, which I discovered in high school in the late ’80s—only to just find out that its first stable release is considered to be around 1995 [5].
- REXX on the Commodore Amiga: not only could applications be extended with this language, but they could also interact with each other. A decade later, when I used REXX on an IBM Mainframe, I still preferred the Amiga’s approach.
- Racket: I can draw in the REPL. For example with Racket Turtle.
- C++: object orientation "for C". I was not mesmerized by templates.
- Clipper: first database usage and relatively simple to create business apps.
- Objective-C: the first implementation of GCD [1], providing a C/C++ style language with fast performance, a clean event-loop solution, and reflective capabilities. However, I never liked the memory handling.
- Machine Code/Assembler on the Apple IIe: I could jump directly into the assembler monitor from BASIC.
- APL: a powerful yet complex way to program using concise expressions. Not ideal for casual use—best suited if you really understand the underlying concepts.
By the way, it’s time for Apple to embrace programming languages on iOS and iPadOS!
[1] http://www.garret.ru/dybase.html
[2] https://wiki.squeak.org/squeak/1762
[3] http://swain.webframe.org/squeak/floppy/
[4] http://toastytech.com/guis/qnxdemo.html
[5] https://en.wikipedia.org/wiki/AutoLISP
[6] https://en.wikipedia.org/wiki/Grand_Central_Dispatch
Aardappel ABC Beatrice Charity Esterel FL Fractran GPM Hope Lean MCPL NESL Oz ProTem Рапира rpython SLC Squiggol UNITY USELESS
Gnu/Linux: Set computer software free so people could discover and learn. 30 years of success and counting. Mind blown.
C: lean, rock-solid durability for 50 years and counting. The connected world runs on C. Mind blown.
AWK, sed, grep, Perl: lean, powerful, rock-solid durability in text and data processing for over 30 years and counting. Mind blown.
SQL and relational data: Querying big data for 50 years and counting, now powering the world in the 21st century. Mind blown.
Old IS Gold!
Thank you! I wasn’t aware of Instaparse or its use of PEGs [1] which gives you the same sense about parsing ambiguities.
> REXX - I thought this was ingenious specifically for file/text processing
Formally the REXX in Amiga was called ARexx and included extensions [2]. REXX [3] itself is not specifically for file/text processing but enables you to connect different environments [4].
[1] https://en.wikipedia.org/wiki/Parsing_expression_grammar
[2] https://en.wikipedia.org/wiki/ARexx
[3] https://en.wikipedia.org/wiki/Rexx
[4] https://www.ibm.com/docs/en/zos/3.1.0?topic=environments-hos...
- MacBASIC: Mac GUI programming w/o Pascal or C https://www.folklore.org/MacBasic.html (which is something I'll never forgive Bill Gates for)
- HyperCARD (also on that list, and agree with almost all points made): It was magic to get into Developer mode and to create stacks --- it's unfortunate that Asymetrix Toolbook didn't get further, nor that the Heizer stack for converting HyperCard stacks to Toolbook wasn't more widely available, and a shame that Runtime Revolution which became Livecode reneged on their opensource effort --- hopefully someone will make good on that: https://openxtalk.org/
Unfortunately, I never got anywhere w/ Interfacebuilder.app or Objective-C....
- Lua: real variables in TeX! (and latex), and one gets METAPOST as well --- even recursion becomes simple: https://tex.stackexchange.com/questions/723897/breaking-out-...
- OpenSCAD: Make 3D things w/o having to use a full-fledged CAD program
- BlockSCAD: Make 3D things w/o typing: https://www.blockscad3d.com/editor/ (though to be fair, https://github.com/derkork/openscad-graph-editor also allows that)
- PythonSCAD: variables and file I/O for OpenSCAD (though to be fair, RapCAD had the latter, it was just hard to use it w/o traditional variables) https://pythonscad.org/
Still working through a re-write of my OpenSCAD library in Python: https://github.com/WillAdams/gcodepreview and am hopeful that a tool like to https://nodezator.com/ will make that graphically accessible (w/o going into OpenSCAD mode to use OSGE).
Then Rust took it a few levels beyond Go.
It’s a really good list, I suspect the languages you add is going to spend on your experience. So I wouldn’t feel too sad if your favorite language is on that list. The author lists Java as a language with an amazing standard library, but something tells me a lot of people will have C#, Go or similar as their Java, and that’s fine!
In other words, Rust doesn't have a type that means "either IO Error or Network Error", so it can't just compose those two behind the scenes into something that you can later decompose. You have to create some tag, and tell the compiler how to apply the tag, because each tag is different.
After a couple of decades in the industry I really prefer explicit error handling because while powerful implicit error handling is also easy to get wrong. Which means I have to go through ridiculous chains of exceptions to find some error someone introduced years ago, instead of being able of heading directly to it and immediately understanding what is going on. So I guess it’s a little contradictory that I prefer the Rust approach, but I think the compromise of added complexity is small enough to make up for the added safety and “manoeuvrability”.
The flip-side is of course that Go’s simplicity is part of the reason it’s seeing a lot of adoption while most other “new” languages aren’t. (Depending on where you live).
I know the biggest complaint with checked exceptions is that people tend to just use catch all exceptions, but that’s just sloppy work. If they’re a junior, that’s when you teach them. Otherwise, people who do sloppy work are just sloppy everywhere anyway.
Simplicity works because it’s made for the real world. As I said I personally think Rust did it better, but if you asked me to come help a company fix a codebase I’d rather do it for a Go one than Rust.
Been on teams where every individual was free to use their best judgement, we didn’t have a lot of documented processes, and… nothing ever went wrong. Everyone knew sloppy work would come back and bite, so they just didn’t ever do it. Deadlines were rarely a problem because everyone knew that you had to estimate in some extra time when presenting to stakeholders. And the team knew when to push back.
On the other hand, I’ve been on teams where you felt compelled to define every process with excruciating detail and yet experienced people somehow screwed up regularly. We didn’t even have hard deadlines so there was no excuse. The difference between implicit and explicit error handling would have not mattered.
At the end of the day, some of these teams got more done with far fewer failures.
“if err != nil {return nil, err}” is the opposite of this philosophy. If you find yourself passing err up the call stack most of the times and most calls may return err, it’s still exception-driven code but without ergonomics.
It’s not simplicity, it’s head butt deep in the sand.
The general idea is that you should pay the cost of handling an error right there where you receive one, even if you're just going to return it. This reduces the incremental cost of actually doing something about it.
If you're given an easy way out, a simpler way of just not handling errors, you either won't even consider handling them, or you'll actively avoid any and all error handling, for fear of polluting your happy path.
I can't say I agree with the approach all the time, but I know I'm much more likely to consider the error flow doing it the Go way, even if I'm comstant annoyed by it.
Doesn't go offer the simplest way of all to "just not handle errors"? Just ignore them. It is an option. You can ignore them on purpose, you can ignore them by mistake, but you can always simply ignore them.
But ignoring by mistake gets caught by linters, in practice.
And then doing it on purpose is almost as noisy as bubbling it, and sure to raise an eyebrow in code review.
My experience is with exceptions in Java/C#, Go errors, and C++ absl::StatusOr. In theory, I'd favor checked exceptions. In practice, I find that I'm always running away from polluting the happy path and coming up with contrived flow when I want to handle/decorate/whatever some errors but not others, and that giving types to checked exceptions becomes a class hierarchy thing that I've also grown to dislike. Both Go and C++/Abseil are indifferent if noisy to me (and C++ annoys me for plenty other reasons). Maybe elsewhere I'd find options better. Maybe.
Zig's way is better.
Error returning is explicit, error returning is declared in the function signature. Error returning plays nice with defer (there is errdefer) and there is limited sugar (try keyword) that makes life easier.
I personally like it better than exceptions (even if it's much noisier for the 95% common case as another poster put it), both of which I've used enough to appreciate the pros/cons of. But that's about it.
I'll probably never use Zig enough to find out.
As others have identified, not in the Go language. In other languages which support disjoint unions with "right-biased" operations, such as Haskell's Either[0], Scala's version[1], amongst others, having to explicitly check for error conditions is not required when an Either (or equivalent) is used.
0 - https://hackage.haskell.org/package/base-4.20.0.1/docs/Data-...
1 - https://www.scala-lang.org/api/3.x/scala/util/Either.html
Processes IRL don’t look like write() three times. They look foo(); bar(); baz(); quux();, which weren’t designed to cooperate under a single context. If only there was an implicit err-ref argument which could be filled by some “throw” keyword and the rest would auto-skip to the “catch”.
Afer a few programs I realized I never really thought about error handling - Go forces you to decide where you want to handle your error, where to inform about it etc.
It is still annoying (especially the boilerplate) but it had a net positive value for me in terms of learning.
Shades of "boring technology"[0] which might better be described as "choose technology that is a good fit for the problem, proven, reliable and proportionate. Spend your innovation tokens sparingly and wisely".
[0]: https://boringtechnology.club/
- running on a VM (now boringly normal)
- using a pure-functional language with immutable terms (the pure-functional language fad has since then come and gone and this is now archaic and passé if anything)
But languages only get stereotyped once. At any rate, it's pretty boring and old.
Say your code divides by 0; the runtime system can't handle 1/0 but you the programmer may have anticipated this and know what to do. This is just someplace you write code right there to handle the case (catch an exception, pattern match the 0 case beforehand, whatever).
Your error, on the other hand, means something you expected to hold didn't; recovery inline from unknown unknowns is a fool's errand. Instead you give up and die, and the problem is now the exception you know how to handle "my child process died".
In brief, a checked exception is part of a method signature, "function wash throws NoSoapException", and the compiler enforces that whoever writes code calling that signature must make some kind of overt decision about what they want to do when that exception comes up. That may mean recovery, wrapping it inside another exception more-suitable to their own module's level of abstraction, deliberately ignore/log it, or just throw a non-checked exception.
So in a sense checked exceptions suit those "error" cases where you do expect the programmer consuming your function to at least decide whether they can handle it, and if they don't you still get all those wonderful features like stack traces and caused-by chaining. In contrast, regular (unchecked) exceptions have the opposite expectation, that the caller probably won't be able to handle them anyway and must opt-in to capture them.
That’s OK. In the same way that when you start to read books, you might like Blyton, Dahl or diving into a Hardy Boys, by the time you’ve been reading fiction for a few decades your tastes might become a bit more philosophical.
The trick is to introduce these things to people when they’re ready for them, and to not treat those who haven’t experienced them as in some way inferior.
I’m not going to shove Proust down someone’s neck or make them feel dumb for not having read any of his work, in the same way I am going to think carefully about whether somebody would benefit from seeing some Prolog.
What does interest me a lot about this list is that it’s not just a “well, look, I found Clojure and think it’s better than Python, YMMV”, or “if you don’t like or understand Haskell maybe you are just too dumb to grok eigenvectors” brag piece.
There is a thought about what you might get from that language that makes it worth exploring. As a result I might revisit OCaml after a brief and shallow flirtation some years ago, and go and take a look at Coq which I doubt I can use professionally but sounds fascinating to explore.
also, what happened to opalang that he mentioned? iirc i read about it back in the day.
Then had to do things in OS/2, and then it was the REXX turn. Shell scripting didn't had to be as basic as I knew from DOS. Some years later moved to bash, and from complex script to long but very powerful oneliners, so it was another shock.
Still was working with OS/2 when learned Perl, and regexes, and its hashes, and all with some interesting semantic approach on that. And for many years it was my "complex" shell scripting language. Python was not as mindblowing or at least had the same kind of impact on me.
Mainstream CPUs expose one or more explicit stack pointers, and in assembly you use that all the time, right?
Instead of "an ilusion", I'd say it's "a convension".
Prolog works bottom-up.
Emacs LISP works inside-out.
[0]: https://www.uiua.org/
(seems like a degenerate, empty, dfn also behaves differently?)
Imagine going from pre World Wide Web - and for many companies pre-email - age of local area networks (for file sharing only) and 38.K modems directly to group and user oriented, email integrated, distributed, replicated, graphics and multimedia integrated, no-code, document oriented, user programmable, fully GUI, secure, distributed database applications.
Lotus Notes blew my mind completely. A truly amazing application at the time.
Common Lisp, which at the moment is the only language I can see myself using unless I decide to make one myself, never really blew my mind, although the little things it does make it hard for me to imagine switching away from it.
> Just that the concepts either didn’t resonate with me or, more often than not, that I had already discovered these same concepts with other languages, just by the luck of the order in which I learnt languages.
These days, base R already includes a native pipe operator (and it is literally `|>`, rather than magrittr's `%>%`).
value = table:function1():function2():function3()
let x1 = one(x); let x2 = two(x1); let x3 = three(x2);
The other advantage of being explicit is you can set breakpoints and see the intermediate values when debugging.
Granted, the same could be said about `three(two(one(X)))`, so it's not specifically a pipe operator problem. It's just that I see it a lot more ever since pipe operators (or their cousins "streams", "extension methods", etc) have been popularized.
My guess is it's because `three(two(one(X)))` across multiple lines would need indentation on each nested function, and end up looking obviously obtuse with too many levels of nesting. Pipes make it look cleaner, which is great at some level, but also more tempting to get caught up in "expression golf" leading to extremely concise but incomprehensible code.
Just some additional commentary too - I think this post quite misrepresents it with some of the comparisons.
Prolog at its core is SLD Resolution [1] (a form of search) over Horn Clauses [2] (first order logic). Queries posted to the runtime are attempts to find a set of values which will satisfy (cause to be true) the query – whilst SQL is founded on relational algebra which more closely aligned with set theory.
Whilst there's probably some isomorphism between satisfying/refuting a logical predicate, and performing various set operations, I'd say it's a bit of a confusion of ideas to say that SQL is based on 'a subset of Prolog'. The author might be thinking about Datalog [3], which is indeed a syntactic subset of Prolog.
[1]: https://en.wikipedia.org/wiki/SLD_resolution [2]: https://en.wikipedia.org/wiki/Horn_clause [3]: https://en.wikipedia.org/wiki/Datalog
1) Formal Syntax and Semantics of Programming Languages: A Laboratory-Based Approach by Ken Slonneger uses Prolog to design/implement languages - http://homepage.divms.uiowa.edu/~slonnegr/ and https://homepage.cs.uiowa.edu/~slonnegr/plf/Book/
2) Defining and Implementing Domain-Specific Languages with Prolog (PhD thesis of Falco Nogatz) pdf here - https://opus.bibliothek.uni-wuerzburg.de/opus4-wuerzburg/fil...
3) Use Prolog to improve LLM's reasoning HN thread - https://news.ycombinator.com/item?id=41831735
4) User "bytebach" gives an example of using Prolog as an intermediate DSL in the prompt to an LLM so as to transform English declarative -> Imperative code - https://news.ycombinator.com/item?id=41549823
5) Prolog in the LLM Era (a series by Eugene Asahara) - https://eugeneasahara.com/category/prolog-in-the-llm-era/
[1]: https://quantumprolog.sgml.net
[2]: https://quantumprolog.sgml.net/container-planning-demo/part1...
https://youtube.com/@thepowerofprolog
https://www.metalevel.at/prolog
You mentioned you're looking for something new that doesn't have to be related to data analytics, well constraint programming (and similar) is basically the mirror problem. Instead of data you have the rules of the "game" and the solver's job can be to: find a solution, find the optimal solution, find all solutions, or prove there is no solution.
Things like scheduling problems, resource allocation problems, etc. A real-world example would be finding the most efficient cell placement when developing a microchip, this is basically an advanced rectangle packing puzzle.
Much like prolog you define the rules (constraints) and the solver takes over from there. Part of the fun is figuring out the most effective way to model a real-world problem in this manner.
The closest thing to Prolog in this domain would be ASP, with Clingo/Clasp being the best solver available. But you also have regular constraint programming (look into MiniZinc or Google's OR-Tools), mixed-integer programming which is mainly for hardcore optimization problems (commercial solvers sell for tens of thousands of dollars), satisfiability modulo theories (often used for software verification and mathematical proofs), etc.
The mind-blowing bit is that this sort of problem is NP-complete but these solvers can find solutions to utterly massive problems (millions of variables and constraints) in milliseconds sometimes.
* C++: The feeling that I can make a-n-y-t-h-i-n-g. (not true but still mostly true) * Ruby: elegant, OO, who-needs-compile-time-if-you-have-unit-tests * Haskell: hard to learn but so worth it, by doing so I became a follow-the-types person * Elm: not hard at all, a well picked subset of Haskell for browser apps only * Kotlin: a well typed Ruby, with the ecosystem of the JVM at your disposal * Rust: when every bit of runtime performance counts (but now without sacrificing safety like with C/C++)
BASIC, but specifically on a TRS-80. I can just type something in and run it. I don't have to wait a week for the teacher to take the card deck over to wherever the mainframe is.
Pascal. That's it, all of it, in those ~4 pages of railroad diagrams expressing the BNF.
C. Like Pascal, but terser and with the training wheels removed. It just fits the way my mind works.
Java. The language itself is kind of "meh", but it has a library for everything.
Perl. Regular expressions and default variables.
And I'd also like to know how different Coq and Lean are. I'm not a mathematician. I'm just a software developer. Is there a good reason for me to pick one over the other?
Lean is designed to be a general purpose programming language that is also useful for mathematical proofs. From that perspective it’s kind of trying to be “better haskell”. (Although most of the focus is definitely on the math side and in practice it probably isn’t actually a better haskell yet for ecosystem reasons.)
If you try either, you’ll likely be playing with a programming paradigm very different than anything you’ve used before: tactic oriented programming. It’s very cool and perfect for math, although I don’t think I’d want to use it for everyday tasks.
You won’t go wrong with either, but my recommendation is to try lean by googling “the natural number game”. It’s a cool online game that teaches you lean and the basic of proofs.
By the way, don’t be scared of dependent types. They are very simple. Dependent types just mean that the type of the second element of a tuple can depend on the value of the first, and the return type of a function can depend on the value passed into it. Dependent types are commonly framed as “types that have values in them” or something, which is a misleading simplification.
C++: pointers, OOP and so much more - good and bad - all in one package
Fortran90: vector programming for humans
Python: general programming for humans
Biggest disappointment: Scratch. Why isn't visual programming more mind blowing?
Anyway, always looking out for these well-regarded "mind-blowing" languages but somehow they never show up in really mind-blowing projects? Would be interesting in this respect to have a list of mind blowing projects that were only possible due to a mind-blowing language.
One issue that i have experienced, which may be related, is that it's hard to have a little bit of controlled type dynamism. That is, once you have a type that is a union with many possibilities, you need to be vigilant not to do operations on it that makes type inference give up and return `Any`. This still bites me. There are some solutions to that in pacakges (e.g. the relatively new Moshi.jl), but I do miss it with language-level support.
https://www.uiua.org
[0]: https://www.youtube.com/c/codereport
And there is this XL language that has very interesting approach to extending the language, but sadly the compiler is not in a runnable state, I could only assess it by the docs.
In recent years, Go blew my mind with it's simplicity and how strong of a case it makes for writing boring code.
It's been decades since writing C/ASM and my guess is what little I remember isn't applicable anymore, but I plan on diving in again at some point if only to better understand Go.