Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disambiguate the meaning of one #16116

Closed
TotalVerb opened this issue Apr 29, 2016 · 91 comments
Closed

Disambiguate the meaning of one #16116

TotalVerb opened this issue Apr 29, 2016 · 91 comments
Labels
dates Dates, times, and the Dates stdlib module needs decision A decision on this change is needed
Milestone

Comments

@TotalVerb
Copy link
Contributor

TotalVerb commented Apr 29, 2016

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.

@toivoh
Copy link
Contributor

toivoh commented Apr 29, 2016

Agreed, the multiplicative identity must always be unitless.

@ivarne ivarne added the dates Dates, times, and the Dates stdlib module label Apr 29, 2016
@omus
Copy link
Member

omus commented Apr 29, 2016

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)

@nalimilan
Copy link
Member

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

@StefanKarpinski
Copy link
Member

StefanKarpinski commented Apr 29, 2016

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.

@stevengj
Copy link
Member

See also Keno/SIUnits.jl#18

@stevengj
Copy link
Member

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

@stevengj
Copy link
Member

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

@timholy timholy added the needs decision A decision on this change is needed label Jul 25, 2016
@timholy
Copy link
Member

timholy commented Jul 25, 2016

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.

@timholy timholy changed the title one for Period is incorrect Disambiguate the meaning of one Jul 25, 2016
@timholy timholy added this to the 0.6.0 milestone Jul 25, 2016
@andyferris
Copy link
Member

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.

@Sacha0 Sacha0 modified the milestones: 1.0, 0.6.0 Dec 15, 2016
@mbauman
Copy link
Member

mbauman commented Jan 5, 2017

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

@stevengj
Copy link
Member

stevengj commented Jan 5, 2017

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.

@timholy
Copy link
Member

timholy commented Jan 5, 2017

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

@andyferris
Copy link
Member

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).

@timholy
Copy link
Member

timholy commented Jan 5, 2017

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.

@andyferris
Copy link
Member

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.

@cstjean
Copy link
Contributor

cstjean commented Jan 6, 2017

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?

@stevengj
Copy link
Member

stevengj commented Jan 6, 2017

one(String) == "" 😈

@andyferris
Copy link
Member

@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?

@martinholters
Copy link
Member

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...

@felixrehren
Copy link
Contributor

felixrehren commented Jan 6, 2017

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) = ""

@timholy
Copy link
Member

timholy commented Jan 6, 2017

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.

@timholy
Copy link
Member

timholy commented Jan 6, 2017

(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}.

@timholy
Copy link
Member

timholy commented Jan 15, 2017

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

@timholy
Copy link
Member

timholy commented Jan 15, 2017

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.

@timholy
Copy link
Member

timholy commented Jan 15, 2017

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?

@timholy
Copy link
Member

timholy commented Jan 15, 2017

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.)

@timholy
Copy link
Member

timholy commented Jan 15, 2017

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:

@timholy
Copy link
Member

timholy commented Jan 15, 2017

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

@timholy
Copy link
Member

timholy commented Jan 15, 2017

Proposal 2: call this function oneunit.

@timholy
Copy link
Member

timholy commented Jan 15, 2017

Proposal 3: call this function unit.

@timholy
Copy link
Member

timholy commented Jan 15, 2017

Proposal 4: call this function oneadd.

@timholy
Copy link
Member

timholy commented Jan 15, 2017

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).

@timholy
Copy link
Member

timholy commented Jan 15, 2017

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)

@TotalVerb
Copy link
Contributor Author

It should be noted that the Proposals 1 are incompatible.

@ajkeller34
Copy link
Contributor

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.

@stevengj
Copy link
Member

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.

@andyferris
Copy link
Member

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

@felixrehren
Copy link
Contributor

felixrehren commented Jan 16, 2017

@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.

@felixrehren
Copy link
Contributor

  • 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.

@andyferris
Copy link
Member

andyferris commented Jan 16, 2017

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.

@timholy
Copy link
Member

timholy commented Jan 16, 2017

@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.

@andyferris
Copy link
Member

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).

@stevengj
Copy link
Member

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.

@stevengj
Copy link
Member

stevengj commented Feb 6, 2017

Closed by #20268

@stevengj stevengj closed this as completed Feb 6, 2017
mdavezac pushed a commit to mdavezac/julia that referenced this issue Sep 13, 2017
According to docstring, `ones` creates an array of `T`, hence it should
call `oneunit` rather than `one`. See JuliaLang#16116 and JuliaLang#20268.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dates Dates, times, and the Dates stdlib module needs decision A decision on this change is needed
Projects
None yet
Development

No branches or pull requests