From 6b8f5eb42ddbd0ed6217c782df3c68c825f4d139 Mon Sep 17 00:00:00 2001 From: Jeff Bezanson Date: Wed, 30 Aug 2017 14:01:28 -0400 Subject: [PATCH] update manual to explain the new mental model of `convert` vs. construct --- doc/src/manual/constructors.md | 23 ---- doc/src/manual/conversion-and-promotion.md | 139 ++++++++++----------- doc/src/manual/methods.md | 2 +- 3 files changed, 65 insertions(+), 99 deletions(-) diff --git a/doc/src/manual/constructors.md b/doc/src/manual/constructors.md index 0d77cdcb56373..a27bb02442def 100644 --- a/doc/src/manual/constructors.md +++ b/doc/src/manual/constructors.md @@ -505,29 +505,6 @@ of its arguments are complex integers, it will return an instance of `Complex{Ou The interested reader should consider perusing the rest of [`rational.jl`](https://github.com/JuliaLang/julia/blob/master/base/rational.jl): it is short, self-contained, and implements an entire basic Julia type. -## [Constructors and Conversion](@id constructors-and-conversion) - -Constructors `T(args...)` in Julia are implemented like other callable objects: methods are added -to their types. The type of a type is `Type`, so all constructor methods are stored in the method -table for the `Type` type. This means that you can declare more flexible constructors, e.g. constructors -for abstract types, by explicitly defining methods for the appropriate types. - -However, in some cases you could consider adding methods to `Base.convert` *instead* of defining -a constructor, because Julia falls back to calling [`convert()`](@ref) if no matching constructor -is found. For example, if no constructor `T(args...) = ...` exists `Base.convert(::Type{T}, args...) = ...` -is called. - -`convert` is used extensively throughout Julia whenever one type needs to be converted to another -(e.g. in assignment, [`ccall`](@ref), etcetera), and should generally only be defined (or successful) -if the conversion is lossless. For example, `convert(Int, 3.0)` produces `3`, but `convert(Int, 3.2)` -throws an `InexactError`. If you want to define a constructor for a lossless conversion from -one type to another, you should probably define a `convert` method instead. - -On the other hand, if your constructor does not represent a lossless conversion, or doesn't represent -"conversion" at all, it is better to leave it as a constructor rather than a `convert` method. -For example, the `Array{Int}()` constructor creates a zero-dimensional `Array` of the type `Int`, -but is not really a "conversion" from `Int` to an `Array`. - ## Outer-only constructors As we have seen, a typical parametric type has inner constructors that are called when type parameters diff --git a/doc/src/manual/conversion-and-promotion.md b/doc/src/manual/conversion-and-promotion.md index 3ce8b87830ef8..141959d1e0bac 100644 --- a/doc/src/manual/conversion-and-promotion.md +++ b/doc/src/manual/conversion-and-promotion.md @@ -41,10 +41,17 @@ of promotion rules defining what types they should promote to when mixed with ot ## Conversion -Conversion of values to various types is performed by the `convert` function. The `convert` function -generally takes two arguments: the first is a type object while the second is a value to convert -to that type; the returned value is the value converted to an instance of given type. The simplest -way to understand this function is to see it in action: +The standard way to obtain a value of a certain type `T` is to call the type's constructor, `T(x)`. +However, there are cases where it's convenient to convert a value from one type to another +without the programmer asking for it explicitly. +One example is assigning a value into an array: if `A` is a `Vector{Float64}`, the expression +`A[1] = 2` should work by automatically converting the `2` from `Int` to `Float64`, and +storing the result in the array. +This is done via the `convert` function. + +The `convert` function generally takes two arguments: the first is a type object and the second is +a value to convert to that type. The returned value is the value converted to an instance of given type. +The simplest way to understand this function is to see it in action: ```jldoctest julia> x = 12 @@ -81,9 +88,7 @@ doesn't know how to perform the requested conversion: ```jldoctest julia> convert(AbstractFloat, "foo") -ERROR: MethodError: Cannot `convert` an object of type String to an object of type AbstractFloat -This may have arisen from a call to the constructor AbstractFloat(...), -since type constructors fall back to convert methods. +ERROR: MethodError: Cannot `convert` an object of type String to an object of type AbstractFloat. ``` Some languages consider parsing strings as numbers or formatting numbers as strings to be conversions @@ -92,93 +97,77 @@ not: even though some strings can be parsed as numbers, most strings are not val of numbers, and only a very limited subset of them are. Therefore in Julia the dedicated `parse()` function must be used to perform this operation, making it more explicit. +### When is `convert` called? + +The following language constructs call `convert`: + + * Assigning to an array converts to the array's element type. + * Assigning to a field of an object converts to the declared type of the field. + * Constructing an object with `new` converts to the object's declared field types. + * Assigning to a variable with a declared type (e.g. `local x::T`) converts to that type. + * A function with a declared return type converts its return value to that type. + * Passing a value to `ccall` converts it to the corresponding argument type. + +### Conversion vs. Construction + +Note that the behavior of `convert(T, x)` appears to be nearly identical to `T(x)`. +Indeed, it usually is. +However, there is a key semantic difference: since `convert` can be called implicitly, +its methods are restricted to cases that are considered "safe" or "unsurprising". +`convert` will only convert between types that represent the same basic kind of thing +(e.g. different representations of numbers, or different string encodings). +It is also usually lossless; converting a value to a different type and back again +should result in the exact same value. + +Notice that some constructors don't implement the concept of "conversion". +For example, `Vector{Int}(5)` constructs a 5-element vector, which is not really a +"conversion" from an integer to a vector. + +Finally, `convert(T, x)` is expected to return the original `x` if `x` is already of type `T`. +In contrast, if `T` is a mutable collection type then `T(x)` should always make a new +collection (copying elements from `x`). + ### Defining New Conversions -To define a new conversion, simply provide a new method for `convert()`. That's really all there -is to it. For example, the method to convert a real number to a boolean is this: +When defining a new type, initially all ways of creating it should be defined as +constructors. +If it becomes clear that implicit conversion would be useful, and that some +constructors meet the above "safety" criteria, then `convert` methods can be added. +These methods are typically quite simple, as they only need to call the appropriate +constructor. +Such a definition might look like this: ```julia -convert(::Type{Bool}, x::Real) = x==0 ? false : x==1 ? true : throw(InexactError()) +convert(::Type{MyType}, x) = MyType(x) ``` The type of the first argument of this method is a [singleton type](@ref man-singleton-types), -`Type{Bool}`, the only instance of which is [`Bool`](@ref). Thus, this method is only invoked -when the first argument is the type value `Bool`. Notice the syntax used for the first +`Type{MyType}`, the only instance of which is [`MyType`](@ref). Thus, this method is only invoked +when the first argument is the type value `MyType`. Notice the syntax used for the first argument: the argument name is omitted prior to the `::` symbol, and only the type is given. This is the syntax in Julia for a function argument whose type is specified but whose value is never used in the function body. In this example, since the type is a singleton, there -would never be any reason to use its value within the body. When invoked, the method -determines whether a numeric value is true or false as a boolean, -by comparing it to one and zero: +would never be any reason to use its value within the body. -```jldoctest -julia> convert(Bool, 1) -true - -julia> convert(Bool, 0) -false - -julia> convert(Bool, 1im) -ERROR: InexactError: convert(Bool, 0 + 1im) -Stacktrace: - [1] convert(::Type{Bool}, ::Complex{Int64}) at ./complex.jl:37 - -julia> convert(Bool, 0im) -false -``` - -The method signatures for conversion methods are often quite a bit more involved than this example, -especially for parametric types. The example above is meant to be pedagogical, and is not the -actual Julia behaviour. This is the actual implementation in Julia: +All instances of some abstract types are by default considered "sufficiently similar" +that a universal `convert` definition is provided in the standard library. +For example, this definition states that it's valid to `convert` any `Number` type to +any other by calling a 1-argument constructor: ```julia -convert(::Type{T}, z::Complex) where {T<:Real} = - (imag(z) == 0 ? convert(T, real(z)) : throw(InexactError())) +convert(::Type{T}, x::Number) where {T<:Number} = T(x) ``` -### [Case Study: Rational Conversions](@id man-rational-conversion) - -To continue our case study of Julia's [`Rational`](@ref) type, here are the conversions declared in -[`rational.jl`](https://github.com/JuliaLang/julia/blob/master/base/rational.jl), -right after the declaration of the type and its constructors: +This means that new `Number` types only need to define constructors, since this +definition will handle `convert` for them. +An identity conversion is also provided to handle the case where the argument is +already of the requested type: ```julia -convert(::Type{Rational{T}}, x::Rational) where {T<:Integer} = Rational(convert(T,x.num),convert(T,x.den)) -convert(::Type{Rational{T}}, x::Integer) where {T<:Integer} = Rational(convert(T,x), convert(T,1)) - -function convert(::Type{Rational{T}}, x::AbstractFloat, tol::Real) where T<:Integer - if isnan(x); return zero(T)//zero(T); end - if isinf(x); return sign(x)//zero(T); end - y = x - a = d = one(T) - b = c = zero(T) - while true - f = convert(T,round(y)); y -= f - a, b, c, d = f*a+c, f*b+d, a, b - if y == 0 || abs(a/b-x) <= tol - return a//b - end - y = 1/y - end -end -convert(rt::Type{Rational{T}}, x::AbstractFloat) where {T<:Integer} = convert(rt,x,eps(x)) - -convert(::Type{T}, x::Rational) where {T<:AbstractFloat} = convert(T,x.num)/convert(T,x.den) -convert(::Type{T}, x::Rational) where {T<:Integer} = div(convert(T,x.num),convert(T,x.den)) +convert(::Type{T}, x::T) where {T<:Number} = x ``` -The initial four convert methods provide conversions to rational types. The first method converts -one type of rational to another type of rational by converting the numerator and denominator to -the appropriate integer type. The second method does the same conversion for integers by taking -the denominator to be 1. The third method implements a standard algorithm for approximating a -floating-point number by a ratio of integers to within a given tolerance, and the fourth method -applies it, using machine epsilon at the given value as the threshold. In general, one should -have `a//b == convert(Rational{Int64}, a/b)`. - -The last two convert methods provide conversions from rational types to floating-point and integer -types. To convert to floating point, one simply converts both numerator and denominator to that -floating point type and then divides. To convert to integer, one can use the `div` operator for -truncated integer division (rounded towards zero). +Similar definitions exist for `AbstractString`, `AbstractArray`, and `Associative`. ## Promotion diff --git a/doc/src/manual/methods.md b/doc/src/manual/methods.md index 862297a52d20b..75f4f6d850325 100644 --- a/doc/src/manual/methods.md +++ b/doc/src/manual/methods.md @@ -862,7 +862,7 @@ julia> p(3) ``` This mechanism is also the key to how type constructors and closures (inner functions that refer -to their surrounding environment) work in Julia, discussed [later in the manual](@ref constructors-and-conversion). +to their surrounding environment) work in Julia. ## Empty generic functions