Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

This is what people mean when they say Haskell is "opinionated."

Haskell shepherds you into separating out IO code from library code to such an extent that literally any function that has an IO action taints the value returned from that function, causing it to be an IO value, and trying to pass that IO value into another function makes the return type of that function IO, too. Parametric polymorphism is the default, too, so it also shepherds you into writing general purpose code. Haskell is full of these little decisions where it just won't let you do something because it's not "correct" code, and they kind of don't care if that makes coding in it a fight against the compiler.

Rust took that philosophy and applied it pointers. Every value has a lifetime and an ownership which makes it quite hard to do things that aren't memory safe.

Both Rust and Haskell wrap values that can fail in little boxes, and to get them out you have to check which type of value it is, and in C# there's nothing stopping you from returning null and not telling anyone that you can return null, and just assuming people will check for null all the time. Haskell has a philosophy of "make invalid code unrepresentable." The concept of a value being in a box, rather than null being a possible value makes it impossible to use that value without getting it out.

People who write Go love that concurrency is easy and Go fmt has enforced a single canonical style. Building these sorts of things into the language goes a long way in getting them adopted and becoming the norm.

I think we saw a rise of the easy, anything-goes, screw-performance scripting languages. I think the next fashion seems to be in enforcing "correct" coding style. They all have their place.



I'm very much in agreement with your assessment here. If you rely on programmers to "do the right thing", some people will break those rules, and systems will suffer varying levels of quality decay as a result. Language level enforcement of key concepts prevents--or at least makes it harder--for folks to make bad decisions in whatever areas those concepts apply. Clojure is another good example where they've provided concurrency primitives that allow you to avoid all the major pitfalls you typically see with multithreaded Java programs. As such, most Clojure code does concurrency the "right way", and in 10+ years of using it, I've never seen a deadlock.


>I think we saw a rise of the easy, anything-goes, screw-performance scripting languages. I think the next fashion seems to be in enforcing "correct" coding style. They all have their place.

Having freedom to do what you want is great. Even if you shoot yourself in the foot, you learn your lesson and become a better developer. But as you work with increasing numbers of people, many making the same mistakes you have made, and especially as you end up having to fix their mistakes, you begin to look for a tool that takes away their ability to shoot themselves in the foot. That was one appeal of Rust when I was learning it. It is a pain to fight the compiler over memory, especially coming from a garbage collected background, but it both protected me from myself and protected me from others. At a certain point, at least on large enough group projects, the benefits of that protection outweigh the costs.


   > "It is a pain to fight the compiler over memory, especially coming from a garbage collected background"
What I don't understand: why don't people stick with garbage collected languages whenever possible?


Lots of reasons.

- You don't want to spend time tuning your GC.

- Response Latency REALLY matters

- Response Throughput REALLY matters

- Memory footprint REALLY matters

- Application + runtime footprint matters

- Memory isn't the only resource you need to manage

- Cost matters

I get that you said "whenever possible" but figured I'd list reasons for "not possible" because I think they have a lot of tie in.

In particular. I could imagine that cost is going to be a driving factor in the future for people to want languages like Rust. They want the absolute fastest app runtime with the lowest resource footprint because AWS charges you for the more memory and CPU time you consume. In that case, the most economical thing to do is favoring the fastest to start and run languages available with the smallest amount of resources.

You might be able to technically do the same job with python, but if you can reduce your operation cost by 10x by switching (or starting) with a slimmer languages, why not?


Because GC addresses only one type of resource: memory, but there exist many other types of resources, and handling them correctly is PITA in most GCed languages.


> in C# there's nothing stopping you from returning null and not telling anyone that you can return null, and just assuming people will check for null all the time

You mean there was nothing?

https://docs.microsoft.com/en-us/dotnet/csharp/nullable-refe...


Doesn't that break a lot of legacy code?

Also, it is certainly an improvement, but having `Foo?` as a type is still less explicit than having `Maybe<Foo>` as a type. If you miss the question mark, you can still have null pointer exceptions.


> Also, it is certainly an improvement, but having `Foo?` as a type is still less explicit than having `Maybe<Foo>` as a type. If you miss the question mark, you can still have null pointer exceptions.

A perfect example of Stroustrup's Rule.

  * For new features, people insist on LOUD explicit syntax.
  * For established features, people want terse notation.
A question mark is concise, but it's just as explicit. The risk of people glossing over it isn't much worse, and avoiding tons of repeated keywords has benefits to comprehension.


The only problem I have is the new syntax has trained me to expect a ? when null is possible, and null is not allowed if ? is missing, but in older libraries not yet updated to C# 8 Nullable Types, not having a ? means null is allowed. So reading the syntax in my IDE is a lot harder as sometimes an un-annotated type means null is allowed and sometimes not. I wish in cross-over projects there was the option to use ! to say null shouldn’t be allowed, thus visually and temporarily distinguishing new from old code, and to that end, the ! could be inserted as an overlay by my IDE, I suppose... Maybe I should try to write an IDE plug-in for this, or have a look for one.


> `Foo?` as a type is still less explicit than having `Maybe<Foo>` as a type.

I actually disagree with this. As long as `Foo?` is checked by the compiler, I think are almost identical in use. It doesn't matter if you don't notice the `?` if it's a compile error to miss the null check.


It's the same if your Maybe<Foo>=Just<Foo>|Nothing, and in fact in that case I often prefer the nullable version, unless there's a dedicated, terse syntax for Maybe-checking built in to the language, the equivalent of null coalescing (Kotlin's ?: (Elvis operator), Typescript's ??), along with optional chaining calls (?.).

If you made a Maybe with a Nothing<String> that comes with a reason why nothing was returned, or any more complicated structure like that, then it's better[1] to use that instead of approximating it with exceptions, null, callbacks with an optional error argument, etc.

[1] In most cases. There's always exceptions, no pun intended.


My view is that `foo?` is a nice sugar for the common Option/Maybe/null case, but that a language is severely missing out if it doesn't also offer general sum types. I don't understand why more languages don't offer them, it seems like it'd be a fairly easy feature to add without breaking backwards compatibility.


Rich Hickey's talk "Maybe Not" has some interesting thoughts on this: https://youtu.be/YR5WdGrpoug


I spent three years making iOS programs in Swift, which uses Foo! and Foo?, and never, NEVER, missed question or exclamation mark. Even if I did, the compiler would complain.


It does break legacy code so you need to opt-in per file or per project. It also doesn't fix legacy code automatically. Any of your dependencies that haven't yet opted in (and thus added the right annotations to their assemblies) to strict null checking are assumed `Foo?` (as they've always been) and may still give NullReferenceExceptions.

Almost all of the BCL (base class library; the system libraries) has been annotated at this point, but there will be plenty of libraries that are not yet on NuGet still.

If you miss the question mark and you've opted in to strict null checks you won't get a null pointer exception, you'll get a compiler error when trying to assign null. (That's why you have to opt-in: it makes `Foo` without the question mark non-nullable.)


I recall `Nullable<Foo>` is equally valid c# if we want a verbose syntax.


yes it does, but you enable it with a flag, per file or per project. Converting an existing solution can make you find a lot of bug...


I hope that this will be wildly accepted and not end like many other good things with "you know, our code currently 'works' .. why would we invest so much effort to make the compiler happy?"


Microsoft is currently updating a lot of code to add this feature, in the end the community will pressure the libraries author so they do the same. I guess that in a few year all popular libs will use the nullable feature.


See also: adoption of TypeScript in the JavaScript community. There is pressure from the ts community for libraries to either be created in ts or for popular libraries to adopt it, and it's become the way a plurality if js devs write js very quickly.


Or it may introduce new ones? This is seriously the most confusing feature I ever saw in C#.

I'll try it on a new project, but converting my existing code that is working fine, no way.


Haskell is not opinionated. All in all, it's probably easier (but misses much of the point of Haskell) to just write "IO" and "do/return" on every function in your program than to use IO in a disciplined way. Haskell even supports this with special do-syntax (and the fortuitously-named "return") to make monadic code look more imperative!

Paying that IO/do/return syntax tax (sin-tax? syn-tax?) is still cheaper than the signature/return boilerplate in competing compiled languages like C and Java. Haskell invites you avoud that syn-tax by writing principled IO.

One of the major complains of Haskell is that it is so expressive and powerful that there are so many incompatible ways of architecting modules. (see the incompatibilities in implementations of Monad Transformers / Effects, Lens, etc)

Rails is perhaps the original "opionated" system. https://guides.rubyonrails.org/getting_started.html


I wonder how much of an uptick Adacore has seen with people using Ada and especially Spark in projects lately. Ada has a different niche than Haskell and Rust, but they're obsessed with software quality and provability. I've only played with Ada, but really liked the code that came out of it. If only they could make strings less painful to deal with.


I'd be very curious if, in the same way that "TypeScript is a superset of JavaScript", there could be a superset of TypeScript that encouraged one to annotate when they're performing IO operations (reading from/writing to the DOM, the network, workers, storage, etc) and if you consumed a function that had said annotation it would further encourage you to annotate that function as well. Something like:

    function writeStringToLocalStorage(key: string, value: string): void, *localStorage {
      // impl
    }

    function persistUsername(username: string): void {
      // ...
      writeStringToLocalStorage('username', username)
      // ...
    }
^ compiler complains that persistUsername writes to localStorage but does not have the *localStorage IO annotation. While I feel like TypeScript does a great job of working with arguments and return values there's still a whole class of issues that can crop up from things like unexpected DOM manipulation that still would be useful in detecting.


> Haskell is full of these little decisions where it just won't let you do something because it's not "correct" code, and they kind of don't care if that makes coding in it a fight against the compiler.

They care more about predicability and compositionality than about a novice's struggles. Professional programmers should prioritise those things. Certainly you can not care about those things for personal projects.

That said, Haskell could of course use plenty of ergonomic improvements, but the ones you describe are not among them.


> I think we saw a rise of the easy, anything-goes, screw-performance scripting languages. I think the next fashion seems to be in enforcing "correct" coding style. They all have their place.

This is the sane way of looking at it.

We noticed that a lot of tasks were not worth the trouble of ensuring correctness, and so dynamic languages too over.

But then a lot of system scaled to a point complexity was hard to manage. And those system had huge economic impact. Which made perf and correctness valuable again, especially since they had a lower price of entry.

I do a lot of Python, usually with dynamic types and mixing IO everywhere. It works surprising well for a ton of cases, and can scale quite far. But I recently wanted to make a system that would provide a plugin system that included a scriptable scenario of input collection that would be chained up to a rendering of some sort. This required to disconnect completely the logic of the scenario - controlled by the 3rd party dev writing it - from the source of the input, and to make the API contract very strict.

It was quite a pleasant experience, seing that Python was capable of all that. Type hints work well now, and coroutines are exactly made for that use case. You can make your entire lib Sans I/O using coroutines as contracts. The automatic state saving and step by step execution is not inelegant.

But you can feel that it's been added on top of the original language design. It's not seamless. It's not shepherding you at all, you need to have discipline, and a deep understanding of the concepts. Which is the opposite of how it feels for other more core features of Python: well integrated, inviting you to do the right thing.

I'm hoping the technology will advance enough so that we eventually get one language that can navigate both side of the spectrum. Giving you the ease of Python/Ruby scripting, data analysis, and agility for medium projects, but letting you transition to Haskell/Rust safety nets and perfs with strict typing + memory safety without GC + good I/O hygiene in a progressive way. Something that can scale up, and scale down.

Right now we always have to choose. I've looked at swift, go, v, zig, nim, various lisps and jvm/.net based products. They always have those sweet spots, but also those blind spots. Which of course people loving the language often don't see (I know some people reading this comment will want to shim in their favorite as candidate, don't bother).

Now you could argue that we can't have it all: choose the right tool for the right job. But I disagree. I think we will eventually have it all. IT is a young field, and we are just at the beginning of what we can do.

Maybe as a transition, we will have a low level runtime language that also include a high level language runtime. Like a rust platform with a python implementation, or what v-lang does with v-script. It won't be the perfect solution, but I'd certainly use something like that.


> a lot of tasks were not worth the trouble of ensuring correctness, and so dynamic languages too over.

I'm not sure about this ... to me it seems the dominant effects were firstly that Javascript is the only language allowed in browsers, and secondarily that nobody had really cracked the usability issues to give us a language that had type-safety and no explicit precompile phase and easy integration with the webserver.

(It doesn't help that a lot of experienced developers are either actively hostile to the concept of developer-usability, or think that their own idiosyncratic habits are the definition of usable and cannot be improved!)

People instead moved their correctness work to unit-testing.


Javascript is only dominant in the browser.

Other scripting languages (python, bash, ruby, php) dominate other parts of automation.


>I'm hoping the technology will advance enough so that we eventually get one language that can navigate both side of the spectrum. Giving you the ease of Python/Ruby scripting, data analysis, and agility for medium projects, but letting you transition to Haskell/Rust safety nets and perfs with strict typing + memory safety without GC + good I/O hygiene in a progressive way. Something that can scale up, and scale down.

[...]

>Now you could argue that we can't have it all: choose the right tool for the right job. But I disagree. I think we will eventually have it all.

The idea of One Language that covers everything (or even most scenarios) is a seductive goal but it's not mathematically possible to create because the finite characters we use to design a language's syntax forces us to make certain concepts more inconvenient than others. This inevitably leads to multiple languages that emphasize different techniques. Previous comments about why this is unavoidable:

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

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

(One could argue that the lowest level of binary 0s and 1s is already the "One Programming Language For Everything" because it's the ancestor of all subsequent languages but that's just an academic distinction. Working in pure 0s and 1s is not a realistic language for working programmers and they'd inevitably find the syntax too inconvenient and thus invent new languages on top of it such as assembly code, Lisp, etc.)


> not mathematically possible to create because the finite characters we use to design a language's syntax forces us to make certain concepts more inconvenient than others

There is a huge number of combinations possible, especially once keywords come into play. I don't think that's the limitation.

On big limitation is that people that are good at creating dynamic languages are bad at creating strict ones, and vice versa.

You can see in a comment bellow than some people talk about swift, Raiky, kotlin like solutions to this problem (as I mentioned in my post it would happen). But of course, they don't have the I/O solution Haskell has, the borrow checker rust has, nor the agility or signal/noise ratio of Python. They have a compromise. A compromise that can be good. Those languages are well designed. But it's not "the ultimate solution", because the don't navigate any end of the spectrum.


">There is a huge number of combinations possible, especially once keywords come into play. I don't think that's the limitation."

The mathematical limitation still remains even if you switch from 1-character symbols like '+' to verbose words like "plus()". You can attempt to invent a new programming language using longer keywords but you'll still run into contradictions of expressing concepts because there's always an implicit runtime assumption behind any syntax that hides the raw manipulation of 0s and 1s. If you didn't hide such assumptions (i.e. "abstractions"), then that means the 0s & 1s and NAND gates would be explicitly visible to the programmer at the syntax layer and that's unusable for non-trivial programs.

There's a reason why no credible computer science paper[0] has claimed to have invented the One Programming Language that can cover the full spectrum of tasks for all situations. It's not because computer scientists with PhDs over the last 50 years are collectively dumb. It's because it's not mathematically possible.

[0] e.g. one can search academic archives: https://arxiv.org/archive/cs


It would be like saying you can't have high level languages because assembly have only a few limited combinations of registers. You can always abstract things away.

In fact, you are making the assumption that there is no more generalist logical concepts we can discover that can abstract or merge what now appears to be conflicting paradigms.

I imagine people said that to Argand. Nah, you can't do that sqrt(-1) thing, you'll run into contradictions.

Given that we have been at the task for less than a century, which is like 1 minute of human history, I'm not inclined to such categorical rejection of the possibility.


Unlambda makes everything equally difficult!


I believe this is a failure of imagination. I'm not a beginner programmer. I've played with a lot of different languages and read about a lot more, and I still have a strong gut feeling that they can be unified if we just figure out the right framework. Note that I, for one, would consider a language that shepherds you towards highly interoperable DSLs to be (close to) a success; something like Racket that could generate efficient native binaries would be really close...

I don't believe for a second that syntax is the obstacle. You can express all the complexity you need with composition. Also, the One True Language will obviously have macros.


> I, for one, would consider a language that shepherds you towards highly interoperable DSLs [...] Also, the One True Language will obviously have macros.

But that just motivates someone else who isn't you to prefer another language that has that "DSL" and macros as the baseline syntax of the new language for convenience. Now you have 2+ languages again.

If someone else prefers not to type out extra parentheses ")))))" to balance things and/or requires highest performance of no GC, then a "Racket/Lisp-like" language can't be the basis of The One True Language.

>You can express all the complexity you need with composition.

True, and to generalize further, a Turing Complete language can be used to create another Turing Complete language. But the ability to build any complexity by composition is itself the motivation to create another programming language that doesn't require the extra work of composition.

For example, one can program in the C Language and combine its primitives to build the "C++ Language" (first C++ compiler was C-with-classes) and the C++ Language can be used to build Javascript interpreter (Netscape browser was written in C++). And then Javascript can be used to build the first Typescript compiler. Thus we might say (via tortured logic) that C Language can let you write a "DSL" as complicated as C++ and Javascript and Typescript. Even though that's true in sense, people don't think of "C Language" as the One True Language. It's the same situation of not thinking of low-level 0s and 1s of NAND gates as being the One True Language even though composition of NAND gates will let you build any other language.


> But that just motivates someone else who isn't you to prefer another language

Sure, they'll want to, and they probably will, but they won't have to due to performance or other constraints, which is how I understood the goal.

> But the ability to build any complexity by composition is itself the motivation to create another programming language that doesn't require the extra work of composition.

That's what the macros are for. All part of the plan.

> ... Even though that's true in sense, people don't think of "C Language" as the One True Language.

C is not certainly not a convenient language for hosting DSLs, due to insufficient abstraction capabilities, but the real missing ingredient is interop between the DSLs. C doesn't make it easy to pass data between them, etc.

NAND gates are not a great comparison. You want to be composing abstractions to create other composable abstractions. You could extend the analogy to composing circuits into bigger circuits, but that's really just converging back to a high level language.


>, but they won't have to due to performance or other constraints,

They have to because a GC runtime is too heavy for embedded environments with low resources.

>That's what the macros are for. All part of the plan.

But there's still the motivation for another language that doesn't require creating the macros. E.g. Lisp macros are so powerful that they can recreate C#'s syntax feature of LINQ queries. That's true -- but C# doesn't require making the macros.

Each programming language has a different "starting point" of convenience. If you try to invent the One Language that can create all other languages' convenience syntax via macros, you've simply motivated the existence of those other languages that don't require the macros.

And the NAND gate is an abstraction. It's an abstraction of decidable logic based on rules instead of thinking about raw voltages. We do combine/compose billions of NANDs abstractions to create higher abstractions.


GC is obviously optional for OTL. That's definitely one of the tricky bits (note: not a syntactic problem).

For the rest, I think your goal definition is too narrow. Removing every last desire for people to create alternatives is an unreasonable bar for literally any artifact, based on human psychology alone. That's explicitly not my goal (see previous comment), and with anyone who does have that goal you need to have an entirely different conversation (which, again, does not involve syntax).


>GC is obviously optional for OTL. That's definitely one of the tricky bits (note: not a syntactic problem).

If a GC language lets the programmer "opt out of GC" to mark a variable or block of memory as "fixed" so the GC doesn't scan it or move it to consolidate free space, how would one annotate that intention unless there's extra syntax for it?

Likewise in the opposition direction: If a non-GC language let's one "opt into GC", you will have ambiguities if you have syntax that allows raw pointers of dynamically calculated addresses to point to any arbitrary offset into a block of memory. That means that memory can't be part of the optional GC which would invalidate the pointer. If you restrict the optional GC language to ban undecidable dynamic pointers, it means you've created the motivation for another language with the syntax that lets you program with the freedom of dynamic pointers!

The general case of "optional GC" that covers all situations and tuning its behavior is tied to syntax because you can't invent a compiler or runtime that can read the mind of the programmer.


Set a flag during the (optional) compile phase that tells the compiler to error out if it can't statically determine where to allocate/free. (No, it's not Halting-complete because the compiler has the option of bailing due to insufficient evidence) Without that flag, it still tries but will include a GC if needed. Same for types, btw.

Ok, fine, you probably want some annotations (like for types). You got me. There's syntax involved. It's still fundamentally a semantics problem. If that can be solved, the syntax will follow.

Your post reads like, "You want to add features? But you'll have to add syntax! It's impossible!" Even if syntax is necessary for GC-obliviousness (it isn't for type inference), it implies no more about whether the project is possible than that for any other feature. Note how far we've strayed from mathematical absolutes about possible strings.

Even on the semantics side, 50 years is far too early to declare defeat. There are no actual impossibilities stopping this, unless you have a formal proof you're not telling us about. Even that would just be a guide of how to change the problem definition, in the same way that Rice's Theorem tells us to add the "insufficient evidence" output to our program verification tools. Have some more imagination.


>Note how far we've strayed from mathematical absolutes about possible strings.

Well sure, we can just theoretically concatenate all the existing programming languages' syntax today into one hypothetical huge string and call _that_ artificial mathematical construct, The One True Language. But obviously, we don't consider OTL solved so "mathematical impossible strings" is constrained to mean "nice strings" advantageous to human ergonomics: reasonable lengths that are easy to read, and easy to type, with no ambiguous syntax causing contradictions in runtime assumptions, no long compile times, etc. E.g. I have no problem typing out balanced parentheses for Lisp but I don't want to do that when writing a quick script in Linux so Bash without all those parentheses is much more convenient.

>There are no actual impossibilities stopping this, unless you have a formal proof you're not telling us about.

The mathematical limitation is that all useful higher level abstractions must have information loss of the lower level it is abstracting. This can be visualized with surjection: https://en.wikipedia.org/wiki/Bijection,_injection_and_surje...

In the wiki diagram, we can think of 'X' as low-level assembly language and 'Y' as higher-level C Language. In C, a line of code to add 2 numbers might be:

  a = b + c;
In the wiki diagram we see X elements '3' and '4' both mapped to Y element 'C'. X-3 and X-4 may be thought of as strategy #3 vs strategy #4 for picking different cpu registers before the ADD instruction and Y-C is the "a=b+c" syntax. In assembly, you manually pick the registers but in C Language you don't because gcc/clang/MSVC compilers do it. Because there are multiple ways in assembler to add numbers that collapse to the equivalent "a=b+c", there is information loss. Most of the time, C Language programmers don't care about registers which is why the C Language abstraction is useful but sometimes you do, and that's why raw assembly is still used. You can't make OTL with the syntax that handles both semantics of assembly and C. If you argue that C can have "inline assembly", that doesn't cover the situation of not having the C Runtime loaded at all that runs prior to "main()". Also, embedding asm in C is still considered by programmers as 2 languages rather than one unified one.

Or we can also think of 'X' as low-level C/C++ language that has numeric data types "short, int, long, float, double". And 'Y' is the higher-level Javascript that only has 1 number type which is a IEEE754 double precision floating point which maps to C languages "double". This means that Javascript's "information lost" is the fine-grained usage of 8-bit ints, 16-bit ints, and 32-bit ints.

If programmer John attempts to design a OTL, he will have to choose which information in the lower layer is "lost" in the runtime assumptions of the higher-level OTL. Since the John's surjection can't cover all scenarios, it motivates another programming language being created. An assumption of GC in the language runtime creates some information loss. Even an optional GC is an abstraction also creates information loss of how to manually manage memory at a lower level of abstraction.


OTL does not need to be surjective onto the set of all binary programs. You only get "information loss" when you try to go backwards, from the end result to the intent. That's reverse engineering, not programming. Now, during translation, the compiler might fill in some information you didn't care about. If you do care about specific instructions and registers for some part of your program, supply them. You probably want to have an assembly DSL that knows about how to integrate with the other code rather than embedding strings. You probably can generate any assembly this way, if just by writing exclusively in the assembly DSL, but the actual requirement is to correctly translate all valid specs.


> You only get "information loss" when you try to go backwards, from the end result to the intent. That's reverse engineering, not programming.

Instead of "information loss", another way to put it is "deliberate reduced choices to make the abstraction useful to ease cognitive burden". That way, it doesn't have connotations about reverse engineering because limitations of surjective mapping is very much about forward engineering.

E.g. I look at Javascript and think forward to engineer how I want to use integers that are larger than 2^53. Javascript's "simpler abstraction of 1 number type" loses the notion of true 64-bit int with a range up to 2^64. Therefore, I don't use Javascript if I need that capability. This means Javascript can't be the OTL for all situations. Your suggestion of Racket-like language as a candidate for OTL has the same problem: it will always have gaps in functionality/semantics/runtime that make others not want to use it and therefore they create Another Language with the desired semantics.

Supplementing the gaps via the ability to write custom DSLs and macros don't solve it. Lisp already has that now and it's not the OTL. If programmer John extends Lisp with macros to simulate monads, he'll spell the macro his way but programmer Bob will spell his macro differently. Now they've created 2 personal dialects of Lisp instead of a larger unified One True Language.

Rereading your comments, I think you're really saying that it's possible to invent the OTL for you, andrewflnr. That's probably true, but unfortunately, that's not a useful answer when the programming community is confused as to why there isn't a universal OTL yet. They're talking about the OTL that everybody can use that covers all scenarios from low-level embedded C Language to scripting to numeric computing to 4GL business languages where SQL SELECT statements are 1st class and don't require double quotes or parentheses or loading any database drivers. Such a universal programming language, if it could exist, would make the "One" in "One True Language" actually mean one.


Most/all languages today take away options. Any OTL would just provide defaults. Details are optional but always possible. I thought I was pretty clear about that re assembly. That's barely even one of the hard parts.

I'm well aware of what it means to have a language for everyone to use. I'm thinking of everything from bootloaders to machine learning to interactive shells. The reason there isn't one yet is that it's really hard. Lots of basic theory about how to think about computation is still being sounded out. Unifying frameworks have been known to take a few decades after that. Still no reason to think it's impossible.

You're just repeating that there will always be gaps, with no evidence except gaps in languages produced by today's rushed, history-bound ecosystem. You're trying to use JS as a illustration of an OTL, which is baffling. Having a limited set of integer sizes would obviously not fly.

I'm apparently not getting the vision across. This is not even a type of thing that exists today, which is why I keep saying to use more imagination. Racket is only close due to its radical flexibility in inputs and outputs.


>Any OTL would just provide defaults.

And you will inevitably have defaults that contradict each other which motivates another language.

Another way of saying "default" is "concepts in the programming language we don't even have to explicitly type by hand or have our eyeballs look at."

What should the OTL default be for not typing any explicit datatype in front of the following _x_ that works for all embedded, scientific numeric, and 4GL business?

  x = 3
Should the default _x_ be a 32bit int or 64int or 128bit int? Or a 64bit double-precision? Or a arbitrary precision decimal (512+ bits memory expandable) or arbitrary size integer (512+ bits expandable)?

Should the default for x be const or mutable? Should the default for x have overflow checks or not? Should default for x be stored in a register or on the stack? Should the name 'x' be allowed to shadow an 'x' defined at a higher scope? What about the following?

  x = 3/9
Should x be turned into approximation of 0.3333... or should x preserve the underlying symbolic representation of 2 rationals with a divide operator (3,div,9)?

The defaults contradict each other at a fundamental level. The default for x cannot be simultaneously be both a 32-bit int and a 512-bit arbitrary precision decimal at the same time. We don't need a yet-to-be-discovered computer science breakthrough to understand that limitation today.

If we go meta and say that the default interpretation for "x = 3" is that it's invalid code and the programmer must type out a datatype in front of x to make it valid, then that choice of default will also motivate another language that doesn't require manually typing out an explicit datatype!

Therefore, we can massively simplify the problem from "One True Language" to just the "One True Datatype" -- and we can't even solve that! Why is it unsolvable? Because the OTD is just another way of saying "read the mind of the programmer and predict which syntax he doesn't want to type out explicitly for convenience in the particular domain he's working in". This is not even a well-posed question for computer science research. Mind-reading is even more intractable than the Halting Problem.

As another example, the default for awk language -- without even manually typing an explicit loop -- is to process text line-by-line from top-to-bottom. This is not a reasonable default for C/Javascript/Racket/etc. But if you make the default in the proposed OTL to not have implicit text processing loop in the runtime, it motivates another language (such as awk) that allows for it. You can't have a runtime that has both simultaneous properties of implicit-text-loop and text-loop-must-be-manually-coded.

Whatever choice you make as the defaults for OTL, it will be wrong for somebody in some other use case which motivates another language that chooses a different default.

>Details are optional but always possible.

Yes, but any extra possibilities will always require extra syntax that humans don't want to type or look at. Again, it's not what's possible. It's what's easy to type and read in the specific domain that the programmer is working in.

>You're just repeating that there will always be gaps, with no evidence except gaps in languages produced by today's rushed, history-bound

Are you saying you believe that abstractions today have gaps but tomorrow's yet-to-be-invented abstractions can be created without gaps and we just haven't discovered it yet because it's really hard with our limited imagination? Is that a fair restatement of your position?

Gaps don't just exist because of myopic accidents of history. Gaps must exist because they are fundamental to creating abstractions. To create an abstraction is to create the existence of gaps at the same time. Gaps are what make the abstraction useful. A map (whether fold paper map or online Google maps) is an abstraction of the real underlying territory. The map must have gaps of information loss because otherwise, the map would be the same size and same atoms as the underlying territory -- and thus the map would no longer be a "map".

The mathematical concept of "average or mean" is an abstraction tool of summing a set of numbers and dividing by its count. The "average" as one type of statistics shorthand, adds power of reasoning by letting us ignore the details but to do so, it also has gaps because there is information loss of all the individual elements that contributed to that average. The unavoidable information loss is what makes "average" usable in speech or writing. You cannot invent a new mathematical "average" which preserves all elements with no information loss because doing so means it's no longer the average. We can write "the average life expectancy is 78.6 in the USA". We can't write "the average life expectancy is [82,55,77,1,...300 million more elements divided by 300 million] in the USA" because that huge sentence's text would then be a gigabyte in size and incomprehensible. You can invent a different abstraction such as "weighted average" or "median" or "mode" but those other abstractions also have "information loss". You're just choosing different information to throw away. We can't just say we're not using enough imagination to envision a new type of mathematical "average" abstraction that will allow us to write an alternative sentence that preserves all information of individual age elements without the sentence being a gigabyte in size.

>JS as a illustration of an OTL, which is baffling.

No, I was using JS as one example about surjection that affects forward engineering. When I say "this means Javascript can't be the OTL for all situations", it's saying all programming languages above NAND gates will have gaps and thus you can't make a OTL.

What's baffling is why anyone would think Racket's (1) defaults + (2) DSL + (3) macros -- would even be a realistic starting point for the universal OTL. The features (1,2,3) you propose as ingredients for universal OTL are the very same undesirable things that motivates other alternative languages to exist! Inappropriate defaults motivates another language with different defaults. The ability to write DSLs motivate another language that doesn't require coding that DSL. The flexibility of coding macros motivate another language that doesn't require coding the macro.


I don't think syntax is the big limitation here; it's library and behavior design.

The One Language concept could still be considered to be accomplished by a language with two syntaxes provided they have a low friction to interoperability.


This sounds more like a you-problem than a programming language problem.

The fact of the matter is that there can be no "perfect" programming language in the sense that it perfectly fits all possible use cases.

So rather than trying to develop or hoping for such language to be developed, a programmer should become multi-lingual. Experiencing first-hand how different programming languages and paradigms approach problems not only broadens the horizon, but also helps with choosing the right tool for the job.

No sane contractor would build a house using nothing but a hammer after all.


A programming language is not tool, like a hammer, with which you build a house. It's a truck full of toolboxes, containing sets of tools made to work in harmony, that you use to build parts of the house that requires a human to deal with it manually.


> I'm hoping the technology will advance enough so that we eventually get one language that can navigate both side of the spectrum.

Me too! I'd love a language at the JavaScript/Python/PHP/Perl level, but in a Swift/Rust style. Possibly with some kind of gradual typing. TypeScript is pretty close to this, but alas its type system isn't sound. And it has to deal with the legacy of JS semantics (like exceptions).


Perhaps Raku (https://raku.org) could hit your sweet spot?


I think Swift more or less hits this sweet spot for me. As mentioned Kotlin is also very close but comes with some baggage. Crystal and Nim are on the horizon and are promising this kind of combination of ergonomics, correctness and performance.


Swift has a long road ahead for the very "low" end, i.e. replacing C or even C++. It's missing, or has extremely awkward versions (`UnsafeOMGPointer`) of various pieces right now, [and some may never even be added][0].

[0]:https://forums.swift.org/t/bit-fields-like-in-c/34651/7


Yeah that makes sense, I guess in my mind I don't see it as a C or C++ replacement as with Rust. To me it fits as a slightly higher level, safe, general purpose language with pretty good performance for most tasks you throw at it. After working with it for a year I feel very productive and that I can trust my code will work if it compiles. A similar feeling to Elm or maybe Rust but yet to spend a longer time with Rust.


Yeah, fair, and I agree with your take; there's just this longstanding idea/goal that Swift can (or will be able to) do it all.


Have you tried Kotlin?


Sounds like you'd like Haxe.


Instead of a reaction to scripting languages, or maybe in addition to, I think the current trends of shepherding languages are reacting to the flexibility of C and, even more so C++. C++ in particular is such a mind-boggling huge language. It presents so many choices that designing anything new involves searching a massive solution space. A task better left to experts.

Newbies (speaking from experience) need a framework to lean on. Something that provides a starting point for solving problems. Opinionated languages provide that out of the box.


I think the "C++ is huge" complaints are a bit overblown. C++ is huge, but most of its new features are designed with backwards compatibility in mind - if the size of the language bothers you, then you can write the limited subset of whatever C++ you know, or even just straight-up C, while making use of new features (auto, foreach, smart pointers) as you see fit. It's an all-you-can eat standard library buffet.


> Both Rust and Haskell wrap values that can fail in little boxes, and to get them out you have to check which type of value it is, and in C# there's nothing stopping you from returning null and not telling anyone that you can return null, and just assuming people will check for null all the time.

F# is the .NET citizen that does the equivalent of the Rust or Haskell stuff.

Either you use an option type (https://fsharpforfunandprofit.com/posts/the-option-type/) which is an easy way of making a function that says 'user says to find a record with the name of Bob, and you will either get a return type of Some record(id:1,name:bob), or you will get a return type of None'

     let GetThisRecord(name) =
          if SomeDatabaseLookup(name).IsSome then
               Some record(SomeDatabaseLookup(name).Value) // not idiomatic but works and is faster than a match
          else
               None
Or you use the Success/Failure type (see railway oriented programming)


The haskell situation sounds like generally a good thing but I am not sure I would like it very much if this also applies to logging.... It does not sound like great fun to have to change the signature of a function when it needs to log something and then change it again if it no longer needs to.


For logging, you can use unsafePerformIO. Of course, you would call it inside a special function that can do logging. In fact, there are functions in Debug.Trace that do exactly that (to standard output).

Similarly, I used unsafePerformIO (again put into a convenient function) to save a checkpoint data in a large computation. The computation is defined pure, but it calls the function to checkpoint, which does in fact IO under the covers.

Remember, type safety is there to help you. As long as the function performing the I/O doesn't affect the outcome of the computation, everything is safe.


> As long as the function performing the I/O doesn't affect the outcome of the computation, everything is safe.

except it's not! Your IO action may not affect the outcome of the computation but it may launch the missiles in background, which changes everything. The less contrived example would be - "computation is fine and is not affected, yet we have our [production cluster deleted / disk space run out / money sent to wrong recepients] by the IO action".


Despite the name, unsafePerformIO isn't automatically akin to undefined behavior in C. It can cause undefined behavior if misused, the most obvious example being the creation of polymorphic I/O reference objects which act like unsafeCoerce—but that would be affecting the outcome of the computation. If the value returned from unsafePerformIO is a pure function of the inputs then the only remaining risk is that any side effects may occur more than once or not at all depending on how the pure code is evaluated. As long as you're okay with that there isn't really any issue with using something like Debug.Trace for its intended purpose, debugging.

There are better ways to handle logging, of course—you generally want your log entries to be deterministic, and the ability to produce log entries (as opposed to arbitrary I/O actions) should be reflected in the types.


Debug.Trace doesn't launch missiles or delete clusters or send money. It might run you out of disk space, but so can safe IO.


Honestly, that will depend on what exactly both you and the GP are calling "logging".

Usually "logging" is semantically relevant, and it better reflect on the return type. But well, it's pretty useless to log the execution of pure code anyway.

I agree that GP seems to be talking about print-debugging (one doesn't go changing his mind about semantically relevant logging), so everything on your comment is on the spot, but generalizing this can lead to confusion.


Standard functional programming methods apply, in this case you would use inversion of control to limit the access to I/O.

If you need to do "semantically relevant" logging from a pure function, just create a pure function to process the semantic relevant part to something generic (like a Text), and call the simple unsafe logging function on the generic result.


Thinking about the systems I work with I can only think of a few cases where logging is semantically relevant (the way I understand it).

One is replaying critical failed requests when a downstream was offline and the other is gathering tracking statistics from apache access logs.

Everything else I would classify as diagnostic, wondering if you would consider that semantic as well.


To clarify it, what I mean by semantically relevant is if it is on the user requirements. It's not semantically relevant if it's there just to make the developer's life easier. So, it seems we are using the same definition.

Every kind of software has some error log, long lived servers tend to have some usage log too, databases tend to have journaling logs, and distributed computing tends to have a retry log. There are other kinds of them, like all those lines a compiler outputs when it tries to work on a program, or the ones a hardware programmer shows while working. Every one of those are there for the user.


Okay, that does sound like a workable solution.


There's a tendency to be very idealistic when talking about IO in haskell, people talk about launching missiles when you ask to print a string and it makes you think we're purist fools. For debugging you can easily drop print statements in without affecting type signatures (with the Debug.Trace package) and this is really helpful but in production you almost never want logging inside pure functions. Think about it, why would you want to log runtime information inside a function that does arithmetic or parses a JSON string? The interesting stuff is when you receive a network request or fail to open a file.


If you have a large application written in Haskell, you're probably already using some sort of abstract or extensible monad for your "business logic", and that means it's usually not hard (in practice) to add a MonadLogger instance to your code.

Also, when you've written Haskell for long enough, you start to write your code in such a way that it's astronomically unlikely that you need to add logging to a pure function. I haven't found myself wanting to do that in years. Haskell has a library to do logging in pure code, but it's unpopular for a reason.


You generally would not put logging into pure functions as that would be fairly pointless. You only log in the IO actions where you can log freely anyway.


In my experience, it actually is a good thing to have to do that, especially in a context-logging world. The actual refactoring is rarely at all difficult in my experience, and by doing so you can make it so logging context is automatically threaded everywhere more ergonomically than other languages even!

And usually when you're logging, it's near other IO anyways. So that makes it even easier.


This.

People just want to get things done, and at some point you start fighting the language, except that the language wins and you lose.

One thing I like about PowerShell is that functions are surprisingly complex little state machines with input streams, begin/process/end pipeline handling, and multiple output streams.

Everything is optional and pluggable. So if you want to intercept the warnings of a function, you can, but it won't pollute your output type.

So in Haskell and Rust, you have "one channel" that you have to make into a tuple. E.g. in Rust syntax:

   fn foo() -> (data,err)
Imagine if you wanted verbose logs, info logs, warnings, errors, etc! You'd have to do something psychotic like:

   fn foo() -> (data,verbose,info,warn,err)
In PowerShell, a function's output is just the objects it returns. E.g. if you do this:

    $result = Invoke-Foo
The $result will contain only your data. Warnings and Errors go to the console. But you can capture them if you want:

    $result = Invoke-Foo -WarningVariable warn -ErrorVariable err
    if ( $warn ) { ... }
In some languages, like Java, strongly typed Exceptions play a similar role. You can ignore them if you like and let them bubble up, or you can capture them, or some subtree of the available types. The only issue is that this mechanism is intended for "exceptional errors" and is too inefficient for general control flow.

There have been proposals for extensible, strongly-typed control flow where functions can have more than just a "return". They can also throw exceptions, raise warnings, yield multiple results, log information, etc... The calling code can then decide how to interact with these in a strongly typed manner, unlike the PowerShell examples above which are one-way and weakly typed.

I'm a bit saddened that Rust didn't go down this path, instead preferring to inherit the current style of providing only a handful of hard-coded control flows, some of which are weakly typed. For example, there's only one "panic", unlike typed exceptions in Java.


> You'd have to do something psychotic like:

You wouldn't have to do this. First of all, if you're talking about something that can error, you'd use a Result, not a tuple (I'm going to use Rust names here):

  fn foo() -> Result<Data, Error> {
Note that you choose both of these types. You can make them do whatever you want. If you wanted to be able to stream those non-fatal things back to the parent, you'd either enhance the Data type to hold them, in which case there'd be no changes, or you'd create a wrapper type for it. You still end up with Result.

Rust also doesn't like globals as much as many languages, but doesn't hate them as much as haskell. Most logging is sent to a thread-local or static logger, so you don't tend to have this in the signature.

In general, many people consider the Result-based system Rust has to be much closer to Java's checked exceptions than most other things. I don't personally because the composability properties feel different to me, but it's also been a long time since I wrote a significant amount of Java code.


> People just want to get things done

If you let people "just get things done", they usually do a shitty job, as we've seen from the last 50 years of software development. People need at least one of unfailing mechanical guidance or impressive levels of restraint. Most people don't have that much restraint (and it's exhausting to keep it up all the time), so the practical option is to have the compiler keep us in check.

If I'm not using Haskell (or equivalent), I usually end up thinking "eh, a quick print statement in the middle of this function won't hurt anybody" and before I know it I've lost the compositionally that makes me love Haskell programming.

> strongly-typed control flow where functions can have more than just a "return"

This sounds to me like what monads give you. ContT, MTL stacks, effect monads, take your pick. There are several ways to get strongly-typed advanced control flow in Haskell.


Hum... Haskell is the one language where people use pluggable middleware everywhere.

But if you program like in C#, you really won't be able to.


> literally any function that has an IO action taints the value returned from that function, causing it to be an IO value, and trying to pass that IO value into another function makes the return type of that function IO, too. Parametric polymorphism is the default, too, so it also shepherds you into writing general purpose code. Haskell is full of these little decisions where it just won't let you do something because it's not "correct" code, and they kind of don't care if that makes coding in it a fight against the compiler.

From a Haskell perspective, and a correctness perspective, and also Rust with its pointer tracking, all this makes sense. It's very helpful for correctness.

Yet, the IO monad "virality" reminds me of Java checked exceptions. Checked exceptions mean every function type signature includes the set of exceptions that function might throw.

When that was introduced, it was thought to be a good idea because it's part of the type-safety of Java and will ensure programmers write code that deals with exceptions correctly, one way or another.

But some years later, people started to argue that listing exceptions in the type signature is causing more software engineering problems than it solves (and C# designers took the decision to not include checked exceptions). Googling "checked exceptions harmful" yields plenty of essays on this.

For checked exceptions, there are people arguing both sides of it. Yet they are pretty much all fans of static typing for the rest of the language; it isn't an argument between people who favour static vs. dynamic typing.

So why are checked exceptions considered harmful by some? On the face of it, there's an argument against verbosity. But the deeper one is about software engineering. What I call "type brittleness".

When you have a large codebase, beautifully and carefully annotated with exact, detailed checked-exception signatures, then one day you have to add a trivial little something to one little function that might throw an exception not already in that function's signature... You may have to go through the large codebase, updating signatures on hundreds or thousands of functions which use the first little function indirectly.

And that's if you have the source. When you have libaries you can't change, you have to wrap and unwrap exceptions all over the place to allow them to propagate via libraries which call back to your own code. Sometimes there is no exception type explicitly allowed by the libaries, so you wrap and unwrap using Java's RuntimeException, the one which all functions allow.

The "viral effect" of so much effort for sometimes tiny changes is a brittleness issue. It leads people to resort to "catch and discard all" try-blocks, to confine the virality Sometimes it's "temporary", but you know how it is with temporary things. Sometimes it isn't temporary because the programmer can't find another clean way to do it while not modifying things they shouldn't or can't.


> When you have a large codebase, beautifully and carefully annotated with exact, detailed checked-exception signatures, then one day you have to add a trivial little something to one little function that might throw an exception not already in that function's signature... You may have to go through the large codebase, updating signatures on hundreds or thousands of functions which use the first little function indirectly.

And you know what? That's probably a good thing. How else can you be sure that all those functions can deal with that exception correctly? If you're adding a new exceptional case to an operation, and rather than handle it locally you decide to punt the issue up the call stack, you should expect that to have far-ranging effects on the rest of the codebase. At that point you have two options for limiting the impact: you can handle the error close to the source, or you can rethrow it as a more general-purpose exception type which is already part of the function's signature (i.e. RuntimeException in Java) with the understanding that any handling of that exception will likewise be generic—typically cancelling or retrying the entire operation.

Of course, libraries which call back in to the user's code can be an issue. (More so in Java than Haskell—so far as I know Java doesn't have any way to make library functions polymorphic in the kinds of exceptions they can throw, whereas in Haskell the exceptions are just part of the type signature so there's no issue with saying "this function throws the same exceptions as the callback".) You may need to temporarily convert the exception into a return value or even provide some out-of-band channel to smuggle it across the library boundary.


> Java checked exceptions

Actually CLU checked exceptions, Modula-3 exception sets, C++ exception specifications.


Good points, all.

I thought of Java only because I'd been reading essays about Java exceptions considered harmful, and then one day I recognised the problem it described, where to change one small function I had to do an absurd number of boilerplate-like edits elsewhere.

I found it quite thought-provoking about "type brittleness" with regard to aspects of the dynamic vs. static typing debate.

I've written in Haskell and SML too, where it didn't feel like the same level of brittleness. Perhaps it's to do with the size of applications and libraries, and how they evolve.

That's why I think of it as a software engineering getting-the-balance-right thing, rather than a correctness vs. prototype-in-a-hurry thing as static-vs-dynamic is often portrayed.


I jump between Java and .NET languages depending on the project/customer, and one thing it bothers me in .NET land, or JVM guest languages, is having to hunt for exceptions, because documentation in some libraries is hardly up to date.

So one ends up putting a couple of catch all handlers in critical code paths, just in case.


"Rust makes it quite hard to do things" generally as a result of that decision. Even just syntactically it's a large overhead. It does force you to explicitly manage lifetimes at every place in your code. Which is a good example of the wrong implementation of the wrong objective.


I agree with your assessment 100%. Does anyone else out there get frustrated with "bare hands" conventions? That's where you have to manually follow a verbose convention or write things like glue manually, when the compiler/runtime could do more of the heavy lifting automatically for us.

For example, say we want to hide low-level threading primitives due to their danger. So we implement a channel system like Go. But we run into a problem where copying data is expensive, so the compiler/runtime has an elaborate mechanism to pass everything by reference and verify that two threads don't try to write the same data. I'm glossing over details here, but basically we end up with Rust.

But what if we questioned our initial assumptions and borrowed techniques from other languages? So we decide to pass everything by value and use a mechanism like copy-on-write (COW) so that mutable data isn't actually copied until it's changed. Now we end up with something more like Clojure and state begins to look more like git under the hood. But novices can just be told that piping data between threads is a free operation unless it's mutated.

To me, the second approach has numerous advantages. I can't prove it mathematically, but my instincts and experience tell me that both approaches can be made to have nearly identical performance. So on a very basic level, I don't quite understand why Rust is a thing. And I look at tons of languages today and sense those fundamental code smells that nobody seems to talk about like boxing, not automatically converting for to foreach to higher level functions (by statically tracing side effects), making us manually write prototypes/headers, etc etc etc.

I really feel that if we could gather all of the best aspects of every language (for example the "having the system on hand" convenience of PHP, the vector processing of MATLAB, the "automagically convert this code to SIMD to run on the GPU" of Julia <- do I have this right?), then we could design a language that satisfies every instinct we have as developers (so that we almost don't need a manual) while at the same time giving us the formalism and performance of the more advanced languages like Haskell. What I'm trying to say is that I think that safe functional programming could be made to look nearly identical to Javascript, or even some of the spoken-language attempts like HyperTalk.

The handwaving around the bare hands stuff is what tires me out as a coder today because fundamentally I just don't view it as necessary. I really believe that there is always a better way, and that we can evolve towards that.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: