Parse text into a type
Give the type a private field nothing outside can set directly, and implement TryFrom<String, Failure> so that Type.tryFrom(text) validates the text and answers a Result instead of a bare value.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
A constructor never contains logic, so it cannot reject bad input. Reading text into a type is a conversion instead:
a private field keeps the constructor from being called with unchecked text, and TryFrom<String, Failure> is the
only way in. Text is a source like any other, so there is no trait and no member of its own for it.
Steps#
Give the type a
privatefield with no default. Without a default, the generated constructor is unusable from outside the type - the only way to build a value from outside is through a function the type itself declares.type Email { private value: String }Implement
TryFrom<String, Failure>.tryFromtakes the text, checks it, and calls the constructor from inside the type, where every field - includingprivateones - can be passed.extend Email with TryFrom<String, EmailError> { static fn tryFrom(text: String): Result<Email, EmailError> { if !text.contains("@") { return Fail EmailError.NotAnAddress(text) } Ok Self(text) } }Add a method to read the field back out. A
privatefield is invisible outside the type, so the type needs to say what it hands out - the text itself, or something derived from it.fn address(): String { value }Call it as
Email.tryFrom(text), or astryInto()in a chain. Under a?the annotation names the target alone and the failure follows from the implementation:fn read(text: String): Result<Email, EmailError> { const address: Email = text.tryInto()? Ok address }Declare a precise error type for what the conversion rejects, rather than reusing
String. A caller can thenmatchon why the text was rejected instead of only being able to print it.
Pitfalls#
- A field without
privatedoes not block the constructor.Email(text)with a public field skips the conversion entirely, so nothing stops an unchecked value from being built - the field has to beprivatefor the conversion to be the only way in. - A
privatefield cannot be reached fromcopyor the constructor outside the type either. Both take the same fields, so aprivatefield with no default disables both from outside, not only the constructor call written out in an example. - There is no
parseon a type. A function namedparsebelongs to a format (Json().parse), never to a value, and a call of one on a type reports that the type has no such member. - The conversion is still an ordinary static function. Calling it is always
Email.tryFrom(text), never a coercion the compiler inserts on its own.
Full example#
type EmailError with Show, Error {
case NotAnAddress(text: String)
fn show(): String {
match self {
.NotAnAddress(text) => "'{text}' is not an email address"
}
}
}
type Email {
private value: String
fn address(): String {
value
}
}
extend Email with TryFrom<String, EmailError> {
static fn tryFrom(text: String): Result<Email, EmailError> {
if !text.contains("@") {
return Fail EmailError.NotAnAddress(text)
}
Ok Self(text)
}
}
fn describe(text: String): String {
match Email.tryFrom(text) {
Ok(email) => "parsed: {email.address()}"
Fail(problem) => "rejected: {problem}"
}
}
print describe("info@example.test")
print describe("not an email")Related#
- Construction - why the constructor cannot validate, and what a factory is for.
- Conversions -
From,Into,TryFromandTryIntoside by side. - Fields -
private, and what a field without a default means for the constructor. - Define an error type - shaping the failure a conversion answers.