Everything in that post applies to the most common programming language out there: TypeScript.
And several popular others such as Rust, Kotlin or Scala.
Everything in that post applies to the most common programming language out there: TypeScript.
And several popular others such as Rust, Kotlin or Scala.
impl std::str::FromStr for Foo {
type Err = ReasonsItIsNotAFoo;
fn from_str(s: &str) -> Result<Self, Self::Err> {
/* etc. */
}
}
And then whenever I've got a string which I know ought to be a Foo, I can: let foo: Foo = string.parse().expect("This {string:?} ought to be a Foo but it isn't");
Since we said foo is a Foo, by inference the parsing of string needs to either succeed with a Foo, or fail while trying, so it calls that FromStr implementation we wrote earlier to achieve that.Compare:
function validateNonEmpty<T>(list: T[]): void {
if (list[0] === undefined)
throw Error("list cannot be empty")
}
function parseNonEmpty<T>(list: T[]): [T, ...T[]] {
if (list[0] !== undefined) {
return list as [T, ...T[]]
} else {
throw Error("list cannot be empty")
}
}
function assertNonEmpty<T>(list: T[]): asserts list is [T, ...T[]] {
if (list[0] === undefined) throw Error("list cannot be empty")
}
function checkEmptiness<T>(list: T[]): list is [T, ...T[]] {
return list[0] !== undefined
}
declare const arr: number[]
// Error: Object is possibly undefined
console.log(arr[0].toLocaleString())
const parsed = parseNonEmpty(arr)
// No error
console.log(parsed[0].toLocaleString())
if (checkEmptiness(arr)) {
// No error
console.log(arr[0].toLocaleString())
}
assertNonEmpty(arr)
// No error
console.log(arr[0].toLocaleString())
For me the `${arg} is ${type}` approach is superior as you are writing the validation once and can pass the precise mechanism for handling of the error to the caller, who tends to have a better idea of what to do in degenerate cases (sometimes throwing a full on Exception is appropriate, but sometimes a different form of recovery is better).This is quite nice in situations where the type system already supports the refinement in question (which is true for this NonEmpty example), but it stops working as soon as you need to do something more complicated. I think sometimes programmers using languages where the TS-style approach is idiomatic can get a little hung up on that, since in those cases, they are more likely to blame the type system for being “insufficiently powerful” when in fact it’s just that the convenience feature isn’t sufficient in that particular case. I presented an example of one such situation in this followup blog post: https://lexi-lambda.github.io/blog/2020/08/13/types-as-axiom...
Just wanted to say that fp-ts (now effect-ts, a ZIO port to TypeScript) author Giulio Canti is a great fan of your "parse don't validate" article. He's linked it many times in the TypeScript and functional programming channels (such as the fp slack).
Needless to say, both fp-ts-derived io-ts library and effect-ts library schema[1] are obviously quite advanced parsers (and in case of schema, there's decoding, encoding, APIs, guard, arbitrary and many other nice things I haven't seen in any functional language).
Or do something more advanced like implement Decoders/Encoders.
My point was merely that the examples being presented in Haskell - and in the context of talking about lists in a very functional, lispy cons-ish kind of way, makes it less accessible for programmers who are using more object-oriented type systems.