Julia: Disambiguate the meaning of `one`

Created on 29 Apr 2016  ·  91Comments  ·  Source: JuliaLang/julia

It seems that this is incorrect:

julia> one(Dates.Second)
1 second

The documentation of one says "Get the multiplicative identity element for the type of x". So the correct return would be Int64(1), since then one(Dates.Second) * Dates.Second(5) == Dates.Second(5) as expected. The current behaviour results in a MethodError which is not desirable.

If I did not miss something obvious, I could make a pull request.

dates decision

Most helpful comment

Proposal 2: call this function oneunit.

All 91 comments

Agreed, the multiplicative identity must always be unitless.

I believe this change would break empty StepRanges with Periods. Potentially this demonstrates a bug with handling empty StepRanges.

julia> Dates.Hour(5):Dates.Hour(1)
5 hours:1 hour:4 hours

The last step of the range is calculated via start - one(stop-start)

I guess one(stop-start) should be replaced with oftype(start, stop-start).

The trouble is that there are (at least) two meanings of one that happen coincide in the absence of units: the multiplicative identity and the additive group generator (of the integers). I suspect that we need to distinguish these with different names and use the right one in each situation in order for our generic code to support units correctly.

See also Keno/SIUnits.jl#18

Maybe oneunit(x::T) for the additive group generator?

Also, step(::UnitRange) = 1 looks wrong for the same reason.

This goes well beyond dates, so I'm adding a "decision" label here. I'm as guilty as anyone of being inconsistent about my use of one: when working on packages related to mathematics with units, then one(x) = 1 seems like the no-brainer fallback for almost anything. Whereas when I'm working on how to pick contrast defaults for displaying images, I want one to work for grayscale and possibly colors and return something with the same "units" as the color object. I agree with the desirability of separating these two notions of one.

With respect to StepRange, note that #17611 may fix this.

This is actually really interesting - which is worse, a method error on one(T) or not having one(T)::T?

I imagine it would be bad if we had zero(Complex{Float64}) = 0, or even zeros(Float64, 10)::Vector{Int}, just because they are valid additive identities.

I almost think of zero(T) and one(T) as constructors for T, but maybe the syntax T(zero) and T(one) would be more natural for that in some respects.

This might be a bit too far from the existing meaning, but identity(*, x) reads nicely. Then one can return the same type.

I've been skimming over the packages that use one(T), and they seem somewhat evenly split between using it as the multiplicative identity and as the additive unit. My inclination would be to continue defining one(x) as the multiplicative identity, possibly of a different type, and define oneunit(x) as the additive unit.

@mbauman, identity(*, x) seems odd given the current meaning of identity. If we want to go that route, it seems closer to define zero(*, x) for the multiplicative identity, but I think that would be confusing too.

How about onemult and oneadd? That way you have to think about which one you mean.

onemult and oneadd

Unfortunately they seem slightly ugly - I think keeping one to mean something would be useful/nice.

oneunit(x) as the additive unit.

is unit(x) a bad choice?

This might be a bit too far from the existing meaning, but identity(*, x) reads nicely. Then one can return the same type.

This is a bit odd-ball, but I was meaning to play with (in a yet-to-exist Identities.jl package) something along the lines of Identity{op} singleton type such that x + Identity{+}() === x and x * Identity{*}() === x and so on, with const one = Identity{*}() and similarly for zero. We can define convert (e.g. convert(Int, ::typeof(one)) = 1) to get an instance of a particular type when necessary, so the user types T(zero) not zero(T). (Using these extra types in practice would require some care to get e.g. reduction loops type stable, but I feel implementing this in a package would let one see how difficult/feasible this kind of approach might be).

I agree with the ugliness...but I worry that we need some kind of deprecation strategy since the return type of one will likely be changing. If we keep the name, then there isn't a good programmatic means of knowing whether a given chunk of code has been updated to the new meaning, so we're stuck with either not issuing any kind of warning or (worse) possibly warning even after someone has updated their code. We wouldn't do the latter, so that means no warning. Maybe that's fine because the docs say that one is the multiplicative identity, but it might cause some problems. If we deprecate one entirely, then we can issue a warning and tell folks to pick one or other other.

Quite true, though I would tend to say that following the behavior of the docstring is a bugfix (which has an effect on others), not a deprecation.

A possible multi-cycle approach would be to introduce unit(T) now (for v0.6) so packages use it for additive identity, and later let one(T) return something that isn't T.

What are examples of types whose multiplicative identity isn't / shouldn't be 1? I know about group theory, but are there more prosaic applications within Julia?

One example that springs to mind is with matrices. one(::Matrix) currently returns the identity matrix. But why couldn't it return 1 instead? Because 1 is not of the same type / part of the same group? Then why is one(Dates.Second(5)) == 1 acceptable?

one(String) == "" 😈

@cstjean I think unit packages struggle with this, for an example I made up: 1 * 1metre = 1metre but 1metre * 1metre is 1(metre^2).

So what should one(1metre) be?

I have to confess I'm still struggling with what these two one versions should do _exactly_. (I'll be using onemult and oneadd in the following because they are unambiguous, not because I want to endorse them.)

  • For onemult(x), it's definitely: "Returns the multiplicative identity of x, i.e. onemult(x) * x == x * onemult(x) == x." Probably also: "Alternatively, it accepts the type of x, i.e. onemult(T) == onemult(x::T)." But that would permit onemult(x::AbstractMatrix) = one(eltype(x)). Now "has to be of the same time as x" is too strict---think of the units example. So "of the same type as x if possible"? That is a) a weak promise and b) then onemult(x::AbstractArray) = one(eltype(x)) together with a specialization for AbstractMatrix returning the identity matrix would still make sense. Presently one(rand(3)) errors---should onemult(rand(3)) return 1.0? Or should onemult(x) only be defined if x*x is? That would be a little closer to our present one definition, I guess.
  • For oneadd(x) it might be: "Returns the additive group generator of the group to which x belongs, if it exists." That covers oneadd(13) == 1, oneadd(Dates.Second(13)) == Dates.Second(1), and oneadd(13metre)=1metre (to pick up that example). But leaves oneadd(12.5) and oneadd(12.5metre) open. For the former "Otherwise, falls back onemult(x)" would work, but apparently that fails for the latter. Actually, oneadd(12.5metre) gives me a headcache for another reason, too: Assuming 1kilometre==1000metre, then oneadd(1kilometre) == 1metre? Why is 1 metre special with respect to addition?
    Or is this just "the closest thing to 1 you can get with type of x"?

So to sum up, IMHO onemult(x) seems to be relatively well-defined, the main question begin whether it should only be defined for cases where x*x is defined, while the additive group generator definition for oneadd(x) does not seem close to covering all the cases we need. What are the typical use cases where the use of one(x) (==onemult(x)) is problematic? What properties of the return value would be needed there?

I have the feeling we should agree on the doctrings of the generic functions first before bikeshedding their names...

This is really interesting. Here's how I understand the points others made above:

  • For any object x whose type T has +, there is a scalar ring R underneath it generated by addition. This has a one, with one(x::T) = one(r::R). For example, measurements, time and matrices all live "over" R <: Number, so one(x) = 1, or maybe 1.0, in these cases
    But unit = oneadd returns a generator of type T. So unit(12metre) = 1metre, and the unit of a 5x5 Array is eye(5). We could have unit(12km) = 1km: because metres and kilometres are different(ly-scaled) structures they can have different units, and keep one(1metre) = one(1km) = 1

If T allows * as well as +, and * is distributive with +, then there is an injection of R into T mapping one(r::R) to one(x::T), so the above one should "right" for * as well -- as far as I can tell?

That covers most cases, but if * is not distributive for +, then T,+,* is weird and all bets are off anyway. If several/weird products are in play, then one(x::T) could be extended to one(x::T,product)?

  • If x has type T with * but no +, then there is no unit, and the type author has to figure out if there is a one and what it is. E.g. one(x::String) = ""

Folks have made some very good points.

  • The "weak promise" point (@martinholters, also implicit in @cstjean) is indeed troubling. I think we should say something like this: "onemult(T) returns the multiplicative identity for objects of type T, and is of type T itself. onemult should not be defined when this is not possible." We'll likely need a HasOne trait to allow code to be safe.
  • @felixrehren, I don't think there is an additive generator for matrices, so I don't think unit = oneadd should be defined in such cases.
  • The Float64 example is nice for showing that oneadd can't be the additive group generator. For Float64, 1.0 is special because it's the generator for all integers, and those in turn generate the rationals under division. Since floating-point numbers correspond to rationals, I think it's fair to call it the "additive group or field generator, if the type is a field".
  • I'm coming to the conclusion that oneadd should not be defined for Unitful quantities. Indeed, perhaps the generic definition of oneadd is oneadd(::Type{T}) = convert(T, 1), a conversion which should fail when objects of type T cannot be equivalent to integers. We don't even need a function for that.

So maybe the plan is to simply enhance the docstring for one to something like this:

    one(x)::typeof(x)
    one(::Type{T})::T

Return the multiplicative identity, `one(x)*x == x*one(x) == x`. You may pass an object `x` or just its type `T`. `one` must return an object of type `T`; if there is no multiplicative identity of type `T` (e.g., for `Vector{Int}` or `12meter`), a `one` method should not be defined. See also `HasOne`.

If you want a number equal to 1 but of type `T`, use `convert(T, 1)` or `oftype(x, 1)` instead.

The implication is that nothing resembling one(12meter) should exist. I should make sure @ajkeller34 is looped into this discussion.

One advantage of this is that it becomes just a docstring change.

(EDITED) Though worth pointing out that under this proposal, one(Day(1)) has to die, and that convert(Day, 1) doesn't really make sense. Therefore CC also @quinnj. Presumably the main motivation for these is to construct ranges, but as I've argued elsewhere one should be forced to specify the step for unitful quantities. Hence one can't have a UnitRange{Day} but one can have a StepRange{Day}.

Dates issue filed #19896

From a theoretical perspective, I love that conclusion. From a practical point of view, there apparently have been many uses of one conflicting with this restrictive definition. Question is: what would they actually require? Can they all use 1 or convert(T, 1) instead?

Can they all use 1 or convert(T, 1) instead?

I think that's the first thing to try. If anyone knows of good examples of "misuses" of one, might be worth seeing if it works.

I would rather define one as a multiplicative identity (of the same type if possible, and otherwise of the same precision if possible), and oneunit(T) as T(one(T))

That "if possible" leaves an awful lot of wiggle room. As @martinholters says above, should we define one(v::AbstractVector) = one(eltype(v))? This definition works generally: it is returning the multiplicative identity, if one exists. If so, then why would one(::Matrix) return a matrix?

I agree with what you said, Tim. Both onemult/oneadd have many different roles they play. No convention we choose can replicate all their (reals/ints) behaviours for all other types. (In particular, the group generator property is rare, and whoever needs it can surely get it another way.)

We can still define the partial analogues, caller to decide whether it will be useful. one as a scalar and unit as an element makes sense for dates, measurements and other things, so let's provide it?

I am interested in how the group generator definition would interact with a fixed-point monetary value type. For instance, suppose we had a type Monetary{:USD} which kept track of currency values to cent precision. In this case, the additive group has generators 1 cent and -1 cent, but perhaps oneunit(Monetary{:USD}) might more intuitively mean 1 dollar.

As @timholy mentioned above, this definition also breaks apart for fields. But in fields, the ideals generated by any non-zero element coincide with the entire field, so any value could be a valid oneunit. But would it be reasonable for oneunit(Float64) to return 2.0?

I believe the following definition of oneunit(x) works in all situations:

If x is considered as an element of a free module (possibly vector space) M on some ring (possibly field) R (possibly M itself), and M has a basis S with one element, then return the single element of the most "standard" such basis.

Under this definition oneunit(::Matrix) and oneunit(::Vector) are meaningless except for the special 1×1 case, which is not really worth dealing with.

Of course, this does raise new questions such as whether oneunit(::Complex{Int}) is meaningful. Complex{Int} is a ring in itself, so of course 1+0im is a basis of it as a free module over itself, but I have some reservations about that the additive group generated by oneunit(::Complex{Int}) does not span Complex{Int}.

So does that mean that unit(Float...)=1.0 and unit(Day(1)) is 1 day? because I feel like those really should work

  • onemult(T) / one(T): Unitful numbers are an interesting case for which an unambiguous multiplicative identity exists, that does not correspond to the same type. There's no weak promise here. one(1.0meter) is 1.0. It would certainly be easiest just to change the docstring to say one(T) must return an object of type T, but then what should I call a function to return the multiplicative identity for a Unitful number?

    It seems to me like the complications in defining one for matrices or vectors instead indicate that we shouldn't be defining it if we are making a weak promise. The weak promise is a separate issue from a possibly differing return type. If we make restrictions on one(T), I'd like to propose that one must return a number. I think this would avoid the weak promise in one(v::AbstractVector) and one(::Matrix), and justify omitting one(::String) regardless of the string concatenation operator. You could still define one(::Day). If you want an identity matrix, there's eye for that.

  • oneunit(T): In practice I find myself using T(one(T)) with Unitful numbers, but primarily when I'm trying to make Unitful work with existing code. I'll defer to the mathematicians on this one (pun intended?) and just say that I am fine if oneunit(T) remains undefined for Unitful numbers since it's pretty easy to use a constructor instead.

  • I'd prefer we avoid using unit as a function name. I suspect some set of people would be pretty confused by whatever unit would return for Unitful numbers if it were suddenly defined in Base (unit(2.0m) == m? unit(2.0m) == 1.0m? unit(2.0m) throws an error?) Getting the m out of 1.0m does have its applications, e.g. in display/GUI code, and I need some interface for that. oneunit is less ambiguous and makes it clear what unit would do.

Honestly, I think it's a mistake to try to be too formal about this. Not everything that is useful to say about computational quantities lies within the realm of abstract algebra. Moreover, all the formality in @TotalVerb's "definition" completely falls apart with the words "most standard" — you're right back to an informal description anyway.

For example, the notion of single vs. double precision, and floating-point arithmetic in general, lies outside of most of abstract algebra (since fp arithmetic is not associative). And yet it is a very useful property for one(x) to return something with the same precision, rather than arbitrarily changing the numeric type.

For most Julia methods, we try to define specific methods in the same general spirit as some informal definition, but we rarely have a formal specification. Why are we insisting on a formal specification here?

If one(x) is defined as a multiplicative identity for x, the identity is not of the same type of x, and there is more than one choice, then we should try to choose the most reasonable one according to our understanding of what people want. This should be the same type, if x is dimensionless, and otherwise should be a type of the same shape and precision if x is dimensionful.

Given one, defining unit(T::Type) = T(one(T)) and unit(x) = oftype(x, one(x)) seems to be a no-brainer, so we are mostly arguing about the definition of one here. However, this definition of unit also gives us some useful information about one: if one(x) does not return the same type as x, then it should still return a type that can be converted to the type of x. (This means that one(x::Matrix) should return a matrix, for example.)

@stevengj one(1meter) should return 1, which should not be convertible to the type of 1meter (see @timholy's nice argument at https://github.com/JuliaLang/julia/issues/19896 and elsewhere). Your conclusion would mean that we shouldn't define one(1meter) when it has a perfectly valid definition.

OTOH, T(one(T)) would work since it's calling a constructor and not converting. There's also unit(x) = typeof(x)(one(x)) as a possibility, which doesn't demand convertibility as it defaults to a constructor.

@ajkeller34, sure I'm happy to amend what I wrote tounit(T::Type) = T(one(T)) and unit{T}(x::T) = T(one(x)).

In which case one(1meter) would return 1 according to my criteria because (a) 1 is a multiplicative identity for 1meter, (b) 1 is the same precision as 1meter, and (c) typeof(1meter)(1) is defined.

To me it seems there's a fair bit of consensus that we reaffirm one(x) as the multiplicative identity. Among other things, that means one(Dates.Day) is going to give a different answer than it used to, or that it goes away. It also seems that unit/oneadd, where it exists, is so trivial that I very much doubt we need a function for it.

The key question boils down to the degree to which we must/can/should specify the degree of (non)triviality of one. The two main examples are:

  • both 1 and 1.0 are the multiplicative identity for 1.0meter, so just saying that it returns the multiplicative identity is not a complete specification. one(x) = 1 would work as a fallback for the vast majority of types, yet that's not how we've been doing it so far.
  • should one(A::Matrix) be one(eltype(A)), eye(eltype(A), size(A)...), or not defined?

Saying that it must be of the same type---or not defined at all---has the advantage of being very clear. @ajkeller34, how useful is one(1.0meter), anyway? As compared to 1, for example? In other words, I'm proposing that a type might have a multiplicative identity, but that isn't enough to imply that one(T) exists---that we only define one (1) when there is a multiplicative identity, and (2) that it's of the same type.

It might be almost as clear to say "has the same shape and precision," meaning we go with 1.0 for 1.0meter and Diagonal(fill(one(eltype(A)), n)) for a matrix. I could be persuaded to go that way. I worry a little that "shape and precision" are less clear concepts than might appear---what exactly is the shape of a Pauli operator? (They can be represented as 2x2 matrices, but they exist as abstract entities satisfying a specific algebra, and there are other representations.) But I'd be willing to go with practicality and just avoid thinking about such concerns.

@timholy I'm simply saying that if one(x) remains defined as the multiplicative identity, I don't think it is sane to exclude its use in perfectly well-defined cases (Unitful numbers), simply to avoid ambiguities in definitions elsewhere (matrices). That's why I suggested specifying that one(x) should return numbers, not vectors, matrices, etc. This would imply one(A::Matrix) = one(eltype(A)), one{T}(::Quantity{T}) = one(T), and one(::String) would be an error. This preserves numeric precision, which is the default Julia behavior even though it's not specified in the docstring. However, it does not preserve shape, which may be less clear anyhow as you say. Is it important for generic code that one(::Matrix) returns a matrix? In other words why couldn't you use eye?

I don't see why it is a problem for one(::Matrix) to return a matrix, though that case is not super useful, I agree. My (informal) definition includes both unitful numbers and matrices.

(For Tim's Pauli example, I again don't see the problem. Assuming that you define a special Pauli{i,T<:Real} type to express σᵢ, for example, then you would obviously define one{i,T}(p::Pauli{i,T}) = Pauli{0,typeof(one(T))}() according to the "multiplicative identity of the same shape and precision" informal criteria.)

Moreover, you would want the criterion that typeof(one(x)) == typeof(one(typeof(x))). By this criterion, if one(x::Matrix) returns a Matrix, then one(::Type{Matrix}) should be left undefined. This is the right choice, because one(::Type{Matrix}) = 1 would make prod type-unstable for an array of matrices.

We could use +() and *() to return the identity (neutral element) for addition and multiplication. This is a natural extension, since +(x) and *(x) already return x.

Since we also need to specify a type, we would extend this to +(::Type{T}) and *(::Type{T}).

Could we extend this notation to arbitrary functions? This would make things like testing 0 preservingness really easy for sparse matrices.

@stevengj I don't have a problem with one(::Matrix) returning a matrix, in fact that's fine with me, I was just trying to understand where one is used with matrices, vectors, etc., and why it is advantageous to preserve shape. If the concern is that shape is a less clear concept than it might appear, then my suggestion would avoid that issue altogether I think? On the other hand if it is important for one to preserve shape than my suggestion is a non-starter.

@eschnett, *() doesn't work because you can't pass an instance of the type that you want the multiplicative identity for.

Also +(T) is weird because it would be called for +T, which seems like it shouldn't be defined at all. I think we should stick with one and zero.

After some more pondering: The multiplicative identity should probably belong to the same multiplicative group to which x belongs. So it is undefined for vectors as these don't form a multiplicative group. And it can't be 1 for matrices. OTOH, unitful quantities actually include unitless numbers in their multiplicate group as 1metre * 1metre^-1 == 1metre^0 == 1, so here 1 would be the multiplicative identity.

Another way of defining it, which is consistent with my informal definition and is a bit more concrete:
one(x) should equal x/x for x != zero(x). That means that it should be a matrix for matrix x, and the same precision, but may not be the same type for a dimensionful x.

@stevengj That definition, however, does not quite work for integers.

It does sort of work for integers, since 1 == 1.0. It's just that one(x) == x/x does not really tell you the precision, since precision is not an algebraic concept in the same way. But I agree that at some level you need to supply some human judgement. I don't think that's so bad.

Fair point. But the condition on x is stronger than x != zero(x); indeed we need that x is non-singular (invertible, in general).

Yes, I was just thinking that.

I still think we're over-thinking this by trying to be so formal. I think we want one(x) to be:

  • A multiplicative identity for x.
  • Of the same precision as x.
  • Convertible to the type of x via oneunit(x) = typeof(x)(one(x)). (This also implies that one(x) is a matrix for matrix x.)
  • == to x/x if x is invertible and / is defined. This also implies that one(x) is a matrix for matrix x.

I agree with @martinholters that we should not define one(x) for vectors, non-square matrices, etc. (Saying that it has to belong "to the same multiplicative group" is not quite right, though, because if x is dimensionful then x is not in a multiplicative group.) I agree that "of the same precision" is a little bit imprecise, but in any particular case I think we can make a reasonable choice.

if x is dimensionful then x is not in a multiplicative group

That's a matter of definition. If you allow powers of the units and throw them all in the same set, it is, like in my metre example. Otherwise, like for Dates.Second, it is not, and I think hereone should not be defined.

Well, this is exactly where we have to choose if one(x)::typeof(x), since the different dimensions have different types (if you want to avoid run-time penalties).

To me, I have only ever used one(T) to avoid type instabilities/consequences from using 1 or 1.0.

Mostly, convert(T, 1) would work but might be slower. (Can we just make that pure?). For matrices I strongly prefer to use eye myself, when necessary.

@timholy Should the meaning of one(x) depend on whether x+x is defined?

@andyferris Then maybe one_oftype as a shorter and potentially more efficient version of convert(T, 1) would be what is needed in practice instead of the multiplicative identity?

Then maybe one_oftype as a shorter and potentially more efficient version of convert(T, 1) would be what is needed in practice instead of the multiplicative identity?

If this is just for efficiency, then why introduce a new function if we can just add @pure to the relevant convert methods?

This is great, so many points of view to think through. I think that shows this is a language issue, and that we need enough language (several different precise words) to disambiguate everyone -- a too simplistic solution will bring out the same disagreements among users. I liked Terry Tao's post on the mathematics that Tim linked to, too, although it's even longer than this thread ...

Here's a table showing different roles people have mentioned. Correct me if any missing. (I'm going to simplify on the issue of precision -- as mentioned by @stevengj. But 1 is an ok stopgap, as after applying the operator, the promoted result will always have the right precision again.)

| | for Number | for Array | Measure m or time | String |
|---|---|---|---|---|
| identity(add +) | 0 | zeroes(A,size(A)...) | 0m | n/a |
| id.(scalar *) | 1 | 1 | 1 | n/a |
| id.(nonsc. *) | 1 | eye(A,size(A)...) | n/a | "" |
| +-generator | 1 | n/a | 1m | n/a |

So let's make explicit everyone's (so-far implicit) operator:

  • leave the issue of group-generators and field-of-fractions to the algebra packages
  • add a function identity{T}(x::T, op) that dispatches on the type and the operator. Authors can simply define identity(x::String, ++) = "", for example.
  • modify one to also dispatch on the operator. In particular,

    • one(A::AbstractArray, +) = 1 (scalar one)

    • one(A::AbstractArray, *) = eye(A,size(A)...) (nonscalar one)

      but

    • one(A::DateTime, +) = 1

    • one(A::DateTime, *) = 1

This allows for super-flexible and clear code, and has a simple docstring. And it's logical enough to be strongly extensible: when someone comes along to write a package using Pauli matrices, or Lie algebras, or fourier transforms (where the 'correct' product is convolution), all these objects admit several operations -- yet, by dispatching on the operator, the code can be uniform.

Edit: thanks, @andyferris!

@felixrehren Note you don't need the Val in Val{+} and Val{*} (functions (and thus operators) already are unique types).

So I suppose the second argument could have a default value of *, and it's not even breaking?

@andyferris, in most cases oneunit(T) = T(one(T)) will be evaluated at compile-time. For cases where it is not, we can always define specialized oneunit(T) methods.

If you look at actual usages of one(T), in some cases it is clearly intended to be of type T, but in other cases the result is multiplied by values of type T and/or it is clearly intended to be dimensionless (e.g. the result of x^0 or cond(x)).

Ok, how about this for the multiplicative identity one(x):

  • It should be defined if and only if x * x is defined.
  • It should match x in terms of type, shape, precision, ..., to the same extent that x * x does.

@martinholters, that definition fails if x has units. x*x is defined, but the multiplicative identity does not have the same type.

If the type to support dimensionful quantities supports powers of units to allow the multiplication, shouldn't it also support the zero-th power, allowing a multiplicative identity within that type?
If x * x is defined, then x^n for integer n isn't far away, and then x^0 is the multiplicative identity. But it would be rather strange if x^1, x^2, x^3, ... all had the same type, but x^0 hadn't. I acknowledge the possibility, though, as x^-1 also may not be defined (or be of a different type), but it still feels strange...

There are two flavors of units packages, one that uses explicit storage for the units (often as a Dict, since of course there are 7 primitive SI units and you have to keep track of all possible combinations) and the other in which units are encoded by the type system. The most widely-used packages seem to be the ones that use the type system, which means that meter^2 is a different type from meter. While that introduces greater challenges for type-stability, it allows you to encode a whole vector of items with the same units in no more storage than you'd need for pure numbers, and to not have to implement the unit-multiplication at runtime.

To be fair, @martinholters did say one(x) "should match x in terms of type, shape, precision, ..., to the same extent that x * x does."

I would say that 1 matches 1 metre to the extent that 1 metre^2 does, so I suppose that matches the definition.

The wording that keeps coming to mind for me is the following:

  • one(3meter) == 1 (multiplicative identity)
  • unit(3meter) == 1meter (additive generator)
  • units(3meter) == meter (units(x) == unit(x)/one(x))
  • quantity(3meter) == 3 (quantity(x) == x/units(x))

The problem is that elements of rings with multiplicative inverses are called "units". This is the opposite of this usage. Then again, quantities with units don't form a ring, so it's not entirely applicable but I wish the terminology was more coherent/intuitive.

@StefanKarpinski Perhaps precedent may help with naming. The corresponding functions in Mathematica are QuantityUnit for 3meter => meter and QuantityMagnitude for 3meter => 3.

I still favor oneunit(1meter) == 1meter. It's a bit longer than unit, but it emphasizes the connection with one, is easier to discover via tab completion of one, is less likely to be confused with the function that returns meter, and seems less likely to conflict with user identifiers in units packages.

This isn't coming to a conclusion. I'll going to split it apart into separate issues/proposals and people can use thumbs-up/thumbs-down to vote. EDIT: note that you can vote on each separate comment; if you dislike one proposal but would be fine with either of the other two, you can give two thumbs up and one thumbs down.

Topic 1: the behavior of one, a function for returning the multiplicative identity. Major observations so far (please feel free to edit this list):

  • 1 works as an identity for many types, even when it doesn't match the precision
  • for matrices/vectors, any definition of one that doesn't return a number is problematic particularly if all you have is the type (not the size/an instance) to work with
  • unitful numbers and dates are examples of algebraic objects for which the multiplicative identity is not of the same type

Proposal 1: one(x) must have the same type as x.

Advantages:

  • one(2) === 1 and one(2.0) === 1.0, which is consistent with current behavior
  • This is perhaps the easiest choice we could make in terms of specifying/documenting the behavior of one.

Disadvantages:

  • one(1meter) cannot be defined.

Proposal 2: one(x) must have the same precision as x.

Advantages:

  • one(2) === 1 and one(2.0) === 1.0, which is consistent with current behavior
  • You can define one(2meter) === 1 and one(2.0meter) === 1.0.

Disadvantages:

  • What does precision even mean for an abstract quantity?

Proposal 3: one(x) must be the multiplicative identity for any y of the same type as x, but the behavior is not specified in any more detail than this.

Advantages:

  • You can define one for almost anything

Disadvantages:

  • This definition does not give any particular reason to choose 1.0 over 1 when defining one(::Type{Float64}). (However, returning a matrix for one(::Matrix) would be disallowed by this definition.)

Topic 2: defining the "additive generator," variously called unit, oneunit, and oneadd above.

There is considerable consensus that, if this function is defined at all, oneunit(::Type{T}) must return something of type T, so I won't even put any alternative to that viewpoint up for debate. Issues:

  • convert has "physical meaning" and so convert(T, 1) shouldn't exist for something like unitful numbers. The same applies to oftype since that's implemented via convert.
  • A non-physical method of construction is T(one(T)).
  • There are concerns raised above the uses of unit in the name (https://github.com/JuliaLang/julia/issues/16116#issuecomment-270961599 and https://github.com/JuliaLang/julia/issues/16116#issuecomment-271779241).

Proposal 1: don't define a function for this, T(one(T)) is very simple (and can be added to the docstring of one).

Proposal 2: call this function oneunit.

Proposal 3: call this function unit.

Proposal 4: call this function oneadd.

I'm not putting up a vote for functions that strip units and/or magnitudes for unitful quantities. Reason: that's not something that Base julia has to decide, authors of packages that handle units should feel free to decide that for themselves (or perhaps among other authors of unit-handling packages, if they want to seek a standard).

Proposal 5: one should optionally take an operator as the second argument, so that
one(x, *) returns the multiplicative identity and one(x, +) returns the additive generator.

Advantages:

  • we don't have to think of two different function names

Disadvantages:

  • one might say that a more consistent definition of one(x, +) is zero(x) (i.e., it's strange to have it return the identity for multiplication but the generator for addition)

It should be noted that the Proposals 1 are incompatible.

Regarding issue 1 proposal 2, I wouldn't get too caught up in the distinction between measurement precision and numerical precision. In the context of this discussion, I think it's numerical precision that matters. The numeric type underlying a Unitful quantity should follow the same promotion rules as the numeric types in Base. Otherwise, there wouldn't be a clean way to work with lower numerical-precision, unitful numbers, for which there are certainly applications. Given that, multiplying by one shouldn't promote the underlying numeric type to Int or Float64 arbitrarily. The logic to handle measurement precision is better handled by an external package like Measurements.jl anyway.

T(one(T)) may be simple, but it requires you to type T twice. Worse, typeof(x)(one(x)) is not simple. Hence my preference for providing oneunit.

Constructors can also "misbehave" - think Vector{T}(1).

@timholy Topic 2/Proposal 5: the multiplicative identity and generators should not be mixed, as reflected by everyone's voting. But I think you picked up on an earlier suggestion by me. Here's the proposal to use multiple dispatch which I think is compatible with every other suggestion:

Proposal 5': identity should optionally take an operator as its second argument. Then identity(x, *) is the multiplicative identity and identity(x, +) == zero(x).
one should optionally take an operator as its second argument, and always return a multiplicative identity.

  • one(x) === one(x, *), e.g. one(x::Array{T,n}, *) == eye(n) (or as decided elsewhere)

This would allow the unification of algebraic code for types with several operations, by allowing the user to specify the operation, and allows authors clear definitions without bikeshedding new function names. It is not breaking.

  • optionally, one(x, +) is the scalar multiplicative identity, e.g. equal to 1 for matrices

This last possibility is about disambiguating scalar one from the typed one, which is useful for matrices and dates and measurements, and makes sense mathematically -- because it's a theorem that an addition operation + always induces a scalar product, and hence scalar multiplicative identity 1.

Just to flash an idea I've been pondering for a while...

Topic 1/Proposal 4:

Define one such that one * x = x for all x, etc. If you want a particular type, then T(one) or convert(T, one) should work fine. In this scenario, we may or may not want to define one(T) at all.

@felixrehren, I agree that we could unify one and zero as you suggest. I'd like to treat that separately, though, since the secondary role for one being debated in this issue is one that doesn't have anything to do with its role as an identity-value.

@andyferris I think having a special symbol one whose sole purpose is one * x = x doesn't really help: its only role is one that we don't need, since it doesn't do anything, so we could just not have it.

By constrast, consider

function assign_grade(rawgrade, studentname, curvefactor = one(rawgrade), behavior::Dict, feeling_generous::Bool)
    multiplier = curvefactor
    if behavior[studentname] == :good
        multiplier *= 2
    end
    if feeling_generous
        multiplier *= 3
    end
    rawgrade * multiplier
end

Often the whole point of one is to create a value that can stably replaced.

Well, I think I might be (clumsily) trying to make a similar point, especially with regards to your last sentence.

I would say, given one and it's mathematical properties, then it is perfectly clear what type comes out of T(one) or convert(T, one), which IMO makes it a more useful tool for ensuring type-stability than one(T). One doesn't need a docstring to clarify what will come out of that constructor - it's self-documenting code.

In your example I would suggest something like curvefactor = typeof(rawgrade)(one) or oftype(rawgrade, one).

On the other hand, using such a one and for its "cute" little multiplication rules is frequently not very useful. E.g. I wouldn't suggest curvefactor = one. Maybe it's useful somewhere, in contexts like im where we have const im = Complex(false, true)... here we don't really want a Bool, we just want something small that promote well with many types and have a small run-time overhead. Imagine (no pun intended) a version with zero run-time costs, const im = Imaginary(one).

A dimensionless one(x) (of the same precision and shape as x, which can be converted to the type of x via typeof(x)(one(x))) is useful in a range of circumstances. e.g. another one just came up in cond(::UniformScaling), since cond is dimensionless. Also, a dimensionless one is hard to write efficiently in terms of a dimensionful oneunit, where as oneunit can be written efficiently in terms of one.

Closed by #20268

Was this page helpful?
0 / 5 - 0 ratings

Related issues

felixrehren picture felixrehren  ·  3Comments

arshpreetsingh picture arshpreetsingh  ·  3Comments

ararslan picture ararslan  ·  3Comments

StefanKarpinski picture StefanKarpinski  ·  3Comments

yurivish picture yurivish  ·  3Comments