Remix.run Logo
wwalexander 6 hours ago

This is just validation that is using the type system to indicate the validation has already occurred. I think the real point of “parse, don’t validate” is to make the type system give you structural guarantees that couldn’t exist otherwise (e.g. always having a first/last element in the NonEmpty example from the original article). If you’re just branding the types as “parsed” (in reality, simply validated) you still have to know that the invariants you care about hold when using the “parsed” type (e.g. splitting the email type using “@“ will always yield 2 elements), instead of the structure of the type holding that info inherently (e.g. struct Email { name: String, host: String }).

jerf 5 hours ago | parent [-]

"This is just validation that is using the type system to indicate the validation has already occurred. I think the real point of “parse, don’t validate” is to make the type system give you structural guarantees that couldn’t exist otherwise (e.g. always having a first/last element in the NonEmpty example from the original article)."

It's the same thing. In the latter case, something has validated that your NonEmpty has a first and a last element. It's all validation before you stick it in a type that asserts that the validation is guaranteed to have occurred so every function receiving it doesn't need to do it itself.

Any non-trivial use of a type system will involve making guarantees the type system itself can not actually express [1]. There's nothing wrong with saying "this is a valid email in accordance with my standards" in a type. Merely using the type system to assert "I have some sort of value in the name and host fields" is valid but a degenerate use. "struct Email { name: Name, host: Hostname }" is an even stronger use of the type system, where Name and Hostname are themselves values you can only get by passing some incoming string through a validation process. Asserting that these things exist is just the most basic check possible, but your type still permits {name: "\0\0\0\0\0\0", host: "!"}, whereas under my definition, assuming that Name and Hostname are reasonably defined, that value will not be ever be something that can be witnessed.

In fact in general, while I don't absolutely rigidly apply this, especially in smaller script-like programs, when a "string" appears in my strong types that specifically means "this has unbounded contents". It's an appropriate type for "stuff I got off a network" or "stuff a user typed". What stuff? Don't know. Haven't checked it yet. When I do it'll get a more specific type like a Username or DecodedUTF8String or something else. Thanks to people using way too many "strings" and "ints" in the world I have to constantly explain to my LLM that I want stronger types. I'm yet to find the invocation to put into my CLAUDE.md or equivalent to get it to do it right the first time consistently.

[1]: With a wistful stare into the distance acknowledging the theoretical utopia of dependent types... but it doesn't seem to be coming down from "theoretical" any time soon.

wwalexander 4 hours ago | parent [-]

> It's the same thing. In the latter case, something has validated that your NonEmpty has a first and a last element.

No, it has parsed it into a structure that structurally has at least one element, not just the promise that there ought to be one. From the original “Parse, don’t validate” article:

    data NonEmpty a = a :| [a]
> your type still permits {name: "\0\0\0\0\0\0", host: "!"}

I actually originally wrote it with an array of EmailNameCharacters, etc but didn’t want to overcomplicate the example.