Comment by bri3d
12 hours ago
> Haskell gives you tools to encode these incantations in types so they cannot be forgotten. This is, for my money, the single most valuable thing the language offers a production engineering organization.
Haskell is admittedly, probably the most powerful widely (or even somewhat widely) used language for doing this, but this general pattern works really well in Rust and TypeScript too and is one of my very favorite tools for writing better code.
I also really like doing things like User -> LoggedInUser -> AccessControlledLoggedInUser to prevent the kind of really obvious AuthZ bugs people make in web applications time and time again.
I've found this pattern to be massively underutilized in industry.
I think you csn also goa long way with C++ and templates to represent sny kind of restricted type in the type system. Variants are somewhat clumsy without pattern matching but most tools you can make use of are already there I would say.
In my backend system I represent users with different variant states to avoid a lot of unrepresentable states.
As for underutilization, I think only functional languages, Rust and C++ support variants and that might be one reason: people just make blobs of state and choose which fields to use instead of encoding states and make some combinations unrepresentable. Javascript, Java, C# or Python do not have Variant types to the best of my knowledge.In Ocaml and Haskell and with pattern matching they are very natural. In Rust with enums, same. In C++, they are so so but still usable compared to the others that do not have.
In my load tests I even went, since I launch thousands of clients, with a boos.MSM to drive the test behavior. One state machine per user.
“Parse don’t validate“ seems like the same idea
https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
You do not need Haskell for that eg it works in Python (via pydantic, attrs data classes)
Agreed. Clojure gets this with Mali and Spec. That said, types are such a productivity boost over time that I think they should only be discarded for very good reasons.
This isn't specific to Rust or Typescript. You can do this in basically any language.
Imagine you have to distinguish between unescaped and escaped strings for security purposes. Even with a dynamically typed language, you can keep escaped strings as an Escaped class, with escape(str)->Escaped and dangerouslyAssumeEscaped(str)->Escaped functions (or static methods). There's a performance cost to this, so that's a tradeoff you have to weigh, but it is possible.
Another way of doing this is Application Hungarian[1], though that relies on the programmer more than it does on the compiler.
[1] https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...
> There's a performance cost to this
That part is (de facto) required for dynamically typed languages, but not for statically typed ones where the newtype constructor/deconstructor can be elided at compile time. Rust and C++ especially both do the latter by having true value types available for wrappers that evaporate into zero extra machine code.
But then just this moment I wondered: do any major runtimes using models with no static type info manage to do full newtype elision in the JIT and only box on the deopt path? What about for models with some static type info but no value types, like Java? (Java's model would imply trickiness around mutability, but it might be possible to detect the easy cases still.) I don't remember any, but it could've shown up when I wasn't looking.
Well, java can do escape analysis, so a wrapper with a single field may end up as a local variable of the embedded field.
As for other JVM languages like Kotlin and Scala, they have basically what "newtype" is, but it can only be completely erased in the byte code when they have a single field.
1 reply →
> This isn't specific to Rust or Typescript. You can do this in basically any language.
This just isn't true.
In any dynamic language you would not get these guarantees at compile time. You'd get random failures at runtime. That's not safety of any kind.
Also, part of the goal of languages like Haskell is that they help you think about your code before it runs. All of that is lost.
> Imagine you have to distinguish between unescaped and escaped strings for security purposes
That would be a nightmare in many languages. You'd have to rewrite large parts of the code to be compatible with one or both. And in many languages you'd have to duplicate your code entirely.
In other languages, the result would so ugly, you would never want to touch that code. Imagine doing this with say, templates in C++.
>There's a performance cost to this
There is no performance cost in Haskell! This is entirely undone by the compiler.
Also, because the compiler understands what's going on at a much higher level, you can do things like deriving code. You can say that your classified strings behave like your regular strings in most contexts, like say, they're the same for the purpose of printing but not for the purpose of equality, in one line.
What you cannot do is compile-time safety guarantees, and in languages like Rust type system isn't strong enough to do some advanced compile-time guarantees (via types). So no, you cannot do this in basically any language (unless you turn it into Haskell).
What the parents describe can be done with almost any language.
> You can do this in basically any language.
You can do it in Assembly. That doesn't mean it's cost effective.
And categorically: the issue isn’t what “I’d” do, my habits often match my habits, it’s what other project members will be doing (including future degenerate versions of myself assumed to be some combination of busy, tired, stressed and drunk).
The Confucian philosophy that people act like water coming down a mountain, seeking the path of least resistance comes to play.
Haskell, OCaml, F#, and their ilk can yield beautiful natural domain languages where using the types wrong is cost prohibitive. In languages without those guarantees every developer needs discipline to avoid shortcuts, and review needs increase, and time-pressure discussions rehashed.
Costs are a skill issue ;-)
I'm not convinced it really works well in typescript. the lack of nominal types requires you to remember some pretty hacky incantations if you want something like a newtype wrapping a primitive type
my experience is that ocaml is more powerful than rust for enforcing this sort of type safety, because you have gadts that give you more expressive power, and polymorphic variants and object types (record row types) that give you more convenience. and the module system and functors of course.
you also avoid some abstraction limitations/difficulties that come from the rust borrow checker for places where garbage collection is just fine
It really feels like we’re solving the wrong problem sometimes. If a bad type can crash your application, sure, type safety is one answer but I have to admit I like the erlang approach; if something unexpected happens crash the process (not os process, erlang process) which has a very small blast radius on a well architected system (maybe doesn’t even fail the individual request that caused it). I wish more languages had this let it crash philosophy, it really allows for writing code exclusively for the happy path, safe in the knowledge that a -1 where a “string” should be isn’t going to take down production.
Somehow, it feels like a better solution than these complicated type systems. Does any other language do this outside BEAM?
In a way I agree with you, and I'm not sure that what popular languages embrace or make it easy to follow this philosophy. My sense is that Erlang is still the leader.
But I did want to add something the article also touches on: types can be not only about ensuring safety or correctness at runtime, but also about representing knowledge by encoding the theory of how the code is supposed to work as far as is practical, in a way that is durable as contributors come and go from a codebase.
Admittedly this can come at the cost of making it slower to experiment on or evolve the code, so you have to think about how strongly you want to enforce something to avoid the rigidity being more painful than valuable. But it's generally a win for helping someone new to a codebase understand it before they change it.
Edit: another thought I had is that type mistakes do not always causes crashes. Silent corruption can be much more insidious, e.g. from confusing types which mean something different but are the same at the primitive level (e.g. a string, number or uuid)
> some pretty hacky incantations
I don't really see a big problem here?
EDIT: previously the example in the parent comment was:
---
This seems wrong; the type spelled `Symbol` refers to the boxed interface for symbols[0]. I suspect you meant to write `unique symbol` there, but it can't be used in that position.
I'm not sure if `NewType` in your comment is supposed to stand in for a specific newtype (in which case it probably doesn't need to be generic[1]) or if it's supposed to be a general-purpose type constructor for any newtype (in which case it should take a second type parameter to let me distinguish e.g. `EmailAddress` from `Password`[2]). The use of `unique symbol`s is also only really necessary if you want to keep the brand private to force users to go through a validation function or whatnot, otherwise you can just use string literal types.
I agree these incantations aren't big problems (it all falls out naturally from knowledge of TypeScript's type system, and can be abstracted away as per my comment in [2]), but the fact that you goofed in the very comment where you were trying to make that point is causing me to second-guess myself.
[0]: https://github.com/microsoft/TypeScript/blob/v6.0.3/src/lib/...
[1]: https://tsplay.dev/N7rvBw
[2]: https://tsplay.dev/Ndep0m
4 replies →
> works really well in Rust and TypeScript too
And of course Rust and TypeScript were heavily influenced by Haskell... they just don't mention it and call things differently, to avoid the "monads are scary, I need to write a tutorial" effect. Though it's less about monads and more about things like type classes.
Imitation is the sincerest form of flattery.
Rust's influence was OCaml, not Haskell. Its first compiler was written in OCaml. Its syntax directly looks like OCaml and C++ had a baby. It's got ML smells all over it. Haskell is not the sum of Hindley Milner-esque languages.
Personally, never enjoyed Haskell's syntax (or lack of it) and tendency to overthinking. But I did enjoy SML/NJ and OCaml to some extent.
Rust has typeclasses so that can't be it.
Are type classes scary? PHP has had them since 2012.
They are different things.
4 replies →
You can't enforce purity on the type level in TypeScript and IIRC neither in Rust.
You can't in Haskell either! For example, any function could secretly call `unsafePerformIO` to cause a side effect (and that's not the only example).
I believe `const` functions in Rust are actually be guaranteed to be pure, though I haven't followed that feature closely and there may be nuances.
In most languages purity is a norm rather than enforced by static analysis. I definitely agree that it's much safer to assume that an arbitrary Haskell function is pure than it is to assume that of an arbitrary TypeScript function.