6 ms·
> 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
by bri3d 5mo 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.
- miki123211 5mo agoThis 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-look-wrong/ https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...
- wyager 5mo ago> You can do this in basically any language. You can do it in Assembly. That doesn't mean it's cost effective.
- myst 5mo agoCosts are a skill issue ;-)
- intrinsicallee 5mo agoYou demonstrate well the problem: yes anything that is computable can be than in any computation system. That's not what discussions about tooling are about. If a tool can help enforce some ways of doing things, or if it doesn't constrain people much, that has consequences for the type of work that gets done with them and the systems you encounter running out there that you might be invited or find the need to work with. "I can do it" is exactly the wrong answer. "How can I guarantee that others will do it" is the point being made.
- myst 5mo agoDoes everybody _need_ to do it?
- intrinsicallee 5mo agoEverybody? Need? Obvious answer is no. Some people, teams and orgs can benefit from it. "I don't need it" is missing the point. "Not everybody needs it" is missing the same point from a different direction.
- bonesss 5mo agoAnd 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.
- dasyatidprime 5mo ago> 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.
- gf000 5mo agoWell, 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.
- dasyatidprime 5mo agoEscape analysis that sinks a local allocation is great in itself, but for newtypes for things like “trusted HTML vs plain text”, I feel like the primary benefits are deeply interprocedural. The type constraint is encoding a promise that can be carried from one end of the code base to the other, and where you can know for sure when you're writing a module whether you're on one side of a barrier or the other. I would tend to expect this to result in patterns that aren't well-handled by escape analysis. What I'm imagining for my curiosity about the dynamic case would look more like “JS/Lua/whatever engine detects that in frob(x) calls, x is always shaped like { foo: ‹string› } and its object identity is unused, so it replaces the calling convention for frob internally, then propagates that to any further callers”, and it might do the same thing when storing one of those in fields of other objects of known shapes, etc. until eventually it hits a boundary where the constraint isn't known to hold and has to be ready to materialize the wrapper object there. Kotlin and Scala sound like they're doing the Rust/C++ thing at the bytecode level, if it's being “erased”, so just the static case again but with different concrete levels for machine vs language.
- k_bx 5mo agoWhat 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).
- uecker 5mo agoWhat the parents describe can be done with almost any language.
- thrawa8387336 5mo agoNo you cannot
- uecker 5mo agoThey described the use of abstract data types. One can certainly use those in most languages - for sure in C.
- k_bx 5mo ago> runST :: (forall s. ST s a) -> a > The rank-2 type (that is, the type s is scoped within the parenthesis and can't escape) of runST ensures that the mutable references created inside the computation cannot escape due to being tagged with the type s. Internally, all sorts of imperative nonsense may occur. Externally, the function is pure. The world outside the boundary gets none of the mutation, only the result. C does not have parametric polymorphism, nor rank-2 quantifications, so no, this cannot be done in C.
- uecker 5mo agoThis was not the original claim in the thread I responded to which as "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)." so this was about abstract data types and not polymorphism. Regardless, you can also have some limited parametric polymorphism in C with macros. This is very poor, but parametric polymorphism in Rust is based on monomorphization so it is also quite poor. You can also have higher-order polymorphism in C but then you need to use subtype polymorphism.
- light_hue_1 5mo ago> 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.
- antonvs 5mo ago> You can do this in basically any language. Tell me you haven't understood what a type system does without... A type system mathematically proves, at compile time, what a program can and cannot do. No, you can't "do this in basically any language". It's ironic that "shift left" is generally considered a good thing', but when you point out that you can shift a significant majority of checks left all the way to compile time, they say "no, not like that!"
- dirkt 5mo ago> 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.
- Pay08 5mo agoAre type classes scary? PHP has had them since 2012.
- adastra22 5mo agoThey are different things.
- Pay08 5mo agoWhat are different things?
- zdimension 5mo agoEli5: Haskell type classes are not classes (like Java or PHP classes); they are comparable to Rust traits -- which are different from PHP traits which are comparable to Java/C# interfaces (with default impls; if you just want contracts you have... PHP interfaces). A fundamental difference is that you can instantiate/implement a type class (or Rust trait) for any* type, compared to interfaces where each class declares the interfaces it implements. You can therefore create generic (forall) instances, higher kinded type classes, etc.
- pjmlp 5mo agoThat conflates type classes with extension types, in type theory. Actually in modern Java you can simulate type classes approach with a mix of interfaces and default methods implementations. In C# you can have the experience more straightforward with extensions types introduced in C#13. Then we have yet another way to approach type classes in Scala, with traits and implicits. And so on, as I haven't yet run out of examples.
- ossopite 5mo agoI'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
- cyberpunk 5mo agoIt 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?
- ossopite 5mo agoIn 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)
- d0mine 5mo ago“Parse don’t validate“ seems like the same idea https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ 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)
- christophilus 5mo agoAgreed. 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.
- satvikpendem 5mo agoIt's more similar to "Make invalid states unrepresentable": https://news.ycombinator.com/item?id=40150159 https://news.ycombinator.com/item?id=40150159
- ngruhn 5mo agoYou can't enforce purity on the type level in TypeScript and IIRC neither in Rust.
- matt_kantor 5mo agoYou 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.
- chuckadams 5mo agoYou can compile Haskell code with the -XSafe flag, and this is communicated in the package, so something like backpack can be told to load only safe modules. Still, there's probably plenty of code that's safe but not pure, but that's as good as we're likely to get.
- germandiago 5mo agoI 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.
- mhh__ 5mo agoThis is more of a question of Affordances than type systems as per se e.g. you can do this quite happily in C# or something it's just that the amount of visual clutter is more than the actual type definition.
- WorldMaker 5mo agoI think that's getting better in C# with record types (and primary constructors in general). Real Discriminated Unions should help a lot, and that's finally in Language Preview now.
- satvikpendem 5mo ago"Make invalid states unrepresentable," as I have posted here before: https://news.ycombinator.com/item?id=40150159 https://news.ycombinator.com/item?id=40150159