Conversions
From provides Into for free and TryFrom provides TryInto, text is a source like any other, and the language has exactly five coercions that apply only where a type is expected.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
A conversion between two types is a trait implementation, never a hidden rule of the language. From is the one to
write, or TryFrom where it can fail; Into, TryInto, the trait coercions and Every type has From<Self> all follow
from them without another line of code, and the fifth coercion wraps a value into the Some or the Ok that
is expected of it.
Example#
type Celsius {
degrees: Float
}
type Fahrenheit {
degrees: Float
}
extend Celsius with From<Fahrenheit> {
static fn from(value: Fahrenheit): Celsius {
Celsius((value.degrees - 32.0) / 1.8)
}
}
const boiling = Celsius.from Fahrenheit(212.0)
const freezing: Celsius = Fahrenheit(32.0).into()
print "{boiling.degrees} {freezing.degrees}"Syntax#
extend <Target> with From<Source> {
static fn from(value: Source): Target { ... }
}
value.into() // Free, once From is implemented
Target.tryFrom(value) // Result<Target, Failure>
value.tryInto() // Free, once TryFrom is implemented
Target.tryFrom(text) // Text is a source like any other
extend <Foreign> with From<Mine> { ... } // Your type into somebody else'sRules#
Implementing
From<Source>providesInto<Target>for free.Fahrenheit(212.0).into()exists becauseCelsiusimplementsFrom<Fahrenheit>, not because anybody wroteIntoby hand.A fallible conversion is
TryFrom, and it answers aResult. WhereFromcannot fail,TryFromis for a conversion that can - a percentage that has to stay between 0 and 100 isTryFrom<Int, String>, notFrom.type Percent { value: Int static fn tryFrom(value: Int): Result<Percent, String> { if value < 0 || value > 100 { return Fail "{value} is not between 0 and 100" } Self value } } const percent = Percent.tryFrom 42 print percentTryFromprovidesTryIntothe wayFromprovidesInto, andtryInto()reads both its target and its failure out of theResultthat is expected of it:type Port { value: Int } extend Port with TryFrom<Int, String> { static fn tryFrom(value: Int): Result<Port, String> { if value < 1 { return Fail "{value} is not a port" } Ok Port(value) } } const port: Result<Port, String> = 8080.tryInto() print port.isOk()A
?on the call says the target and not the failure, and the failure then follows from the oneTryFromthe target has for this source:const small: Port = 8080.tryInto()?is the same conversion written in a chain. Where the target has several conversions from one source, the message says so and theResulthas to be written out.A conversion from text is a
TryFrom<String, Failure>like any other.Int.tryFrom "42"andEmail.tryFrom "info@example.test"both answer aResult, and there is noparseon a type: a function namedparsebelongs to a format (Json().parse), never to a value. A type may implementTryFromonce per source, and the call decides which one it means: the argument, and where the argument leaves several standing, theResultthe call is expected to produce.type Port { number: Int } extend Port with TryFrom<String, String> { static fn tryFrom(value: String): Result<Port, String> { Ok Port(1) } } extend Port with TryFrom<Int, String> { static fn tryFrom(value: Int): Result<Port, String> { Ok Port(value) } } const fromText = Port.tryFrom "8080" const fromNumber = Port.tryFrom 8080 print "{fromText.isOk()} {fromNumber.isOk()}"Converting your own type into a foreign one is
extend Foreign with From<Mine>. A package owns an implementation when it owns the type, the trait, or a type named as an argument of the trait - so the package that declaresCelsiusmay writeextend Float64 with From<Celsius>although neitherFloat64norFromis its own. Only the top level of an argument counts:From<List<Celsius>>is not yours. See Coherence.type Celsius { degrees: Float } extend Float64 with From<Celsius> { static fn from(value: Celsius): Float64 { value.degrees } } const degrees: Float64 = Celsius(21.5).into() print degreesEvery type has
From<Self>, generated without being written. A blanket implementation could not be written by hand - it would overlap with every otherFrom- which is what lets a function ask forItem: From<Int>and still accept anItemitself.The language has exactly five coercions, and every one of them applies only where a type is expected. None of them decides what a bare expression means and none of them solves an inference variable on its own:
- a value where a trait it implements is expected,
- a trait value where fewer bounds or a supertrait is expected,
Neverwhere anything is expected,- a literal where a literal type that contains it is expected,
- a value where an
Optionor aResultof its type is expected (rule 8).
Into<Target>is also a type, andinto()on such a value is an ordinary call. A parameter declaredInto<Path>takes anything that converts into aPath, andintois the trait's one required member - the conversion throughFromis what happens for a receiver whose own type has no such member.type Path { text: String } fn open(path: Into<Path>): String { const target: Path = path.into() target.text } print open(Path("notes"))A value wraps itself into
SomeandOkwhere one is expected. A value of exactlyValuewhere anOption<Value>or aResult<Value, Failure>is expected becomesSome(value)orOk(value)- the result of a body andreturn, a binding with an annotation, an argument, a field, a collection element, the result of a closure that declares one. The coercion to a trait type applies first, so aSquarewhere aShape?is expected is boxed and wrapped.NoneandFail(...)stay written.fn find(items: List<Int>, wanted: Int): Int? { for item in items { if item == wanted { return item } } None } fn halved(value: Int): Result<Int, String> { if value % 2 != 0 { return Fail "{value} is odd" } value / 2 } const port: Int? = 8080 const holes: List<Int?> = [1, None, 3] print "{find([1, 2, 3], 2)} {halved(8)} {port} {holes}"A value that already is an
Optionor aResultnever wraps into anOption, so nothing becomesSome(Some(x))silently, and the payload of a writtenSome(...)orOk(...)never wraps itself: the innerSomeof a nestedOptionstays written. Into aResulta value wraps only where it is exactly theValue- anInt?becomes theOkof aResult<Int?, Failure>.Kompiliert nichtconst nested: Int?? = Some(1) // error: Expected `Option<Int64>`, found `Int64`The wrap never solves an inference variable: where the
Valueis still open - an argument offn wrapped<Item>(value: Item?)- the value is checked as it is, andSomeis written.
What this is not#
Into and TryInto are not something you implement. Each of them comes from a blanket implementation over the
other direction. A hand-written Into is refused by a rule of its own - extend Celsius with Into<Float64> is
"Implement From<Celsius> for Float64 instead; Into comes from it", with the note "Write extend Float64 with From<Celsius>" - because the From gives the Into for free while the Into gives nothing back, and coherence
allows the From wherever it would allow the Into. It is Rust's from_over_into lint, made a rule. A hand-written
TryInto overlaps its blanket, and that message names the TryFrom to write.
type Celsius {
degrees: Float
}
extend Celsius with Into<Float64> {
fn into(): Float64 {
degrees
}
}
// error: Implement `From<Celsius>` for `Float64` instead; `Into` comes from itextend Celsius with From<Fahrenheit> {
static fn from(value: Fahrenheit): Celsius {
Celsius((value.degrees - 32.0) / 1.8)
}
}
const freezing: Celsius = Fahrenheit(32.0).into()const value: Int = "42"
// error: Expected `Int64`, found `String`A String never becomes an Int on its own either, for the same reason: there is no coercion from one concrete type
to another, only Int.tryFrom("42") written out. Int is an alias for Int64, which is the name the diagnostic uses.
Related#
- Construction - the static factory functions
FromandTryFromare examples of. - Declaring a type - where
Selfandextendare introduced. - Result - what every fallible conversion in this page answers with.
- Coherence - which package may write which implementation, and the blanket message.