Why does Golang get the units wrong when multiplying Durations

The way that Go handles operations on Durations appears to contradict the normal rules of math and science. We all know that if you multiply a length in feet by a number, you get a length in feet. If you multiply a length in feet by a length in feet, you get a quantity in square feet — an area. This is an an example of a very basic idea. In Go, if you multiply a Duration of 10 seconds by 10, you get an error. If you convert the number 10 to a Duration, it becomes 10 nanoseconds. If you multiply 10 seconds by 10 nanoseconds, the conceptual answer should be 100 billion square nanoseconds, which is one hundred thousand square microseconds. If I have the math wrong it’s because I could not use Go to check it. If you don’t think the concept of square seconds makes sense, you should presumably give an error. The answer given by Go is 100 seconds. What conceptual model is Go using to support this answer? I understand that Go made the decision not to do automatic type conversion, but that does not seem like a reasonable justification for nonsensical semantics of operations on values that have units attached.

A programming language type system, which does not map one to one to physical units. Much like multiplying two ints doesn’t result in an int2, multiplying two Durations also results in just a Duration, type wise.

Note that this works fine (in your desired way) for untyped constants: 5*time.Second. It’s just that the type system doesn’t allow you to multiply two entities of different types (int and Duration, for example), so when your multiplier is stored in a variable it must also be a Duration even though it does not represent a unit of time. If you like, you can consider a Duration a type that holds a unit of time or a unitless value. You just can’t distinguish the two cases using the Go type system.

I cannot confirm this: The following code returns 100ns.

EDIT: Irrelevant because constants are typeless. See @Rio_M’s reply.

var t1 time.Duration
t1 = 10
fmt.Println(t1 * 10)
// Output: 100ns

(Playground link)

Can you paste the code that errors out when multiplying a duration with an int? I suspect there might be a different reason for the error.

It is true that Go has no types like “SquareDuration”, but I don’t know of any programming language that incorporates SI units into its type system, including square and cube variants of each SI unit, as well as the resulting conversions when doing multiplications

You can always define your own SI types if required, for example:

type feet int
type squarefeet int

func (f feet) times(ft feet) squarefeet {
    return squarefeet(f*ft)
}

func main() {
    var f1, f2 feet
    f1 = 7
    f2 = 6
	fmt.Println(f1.times(f2))
}

(Playground link)

Probably this is correct example for error:

package main

import (
    "fmt"
    "time"
)

func main() {
    var t1 time.Duration
    var nn int
    nn = 10
    t1 = 10
    fmt.Println(t1 * nn) //required: fmt.Println(t1 * time.Duration(nn))
}
1 Like

Right, my fault - constants are typeless.

To elaborate a bit on the correct code example, the point of disallowing mixing of different types is safety and simplicity. Safety, because this keeps programmers from making mistakes. Multiplying a date with an int, for example, makes no sense. Adding inches to centimeters is ambiguous. What is the resulting type of 10in + 10cm? Inches or centimeters? Should the operation implicitly convert one of the values to the resulting type? What if I pass a Fahrenheit value to a function that expects Celsius? Should the value get converted (with the risk of getting rounding errors that might make a comparison for equality return the wrong result)?

Trying to implement answers for all these problems would only result in a bloated type system. Bloated type systems lead to slow compilation and complex specifications.

Go’s solution is to have the programmer decide what they want to achieve, and then code this explicitly, as in @Rio_M’s code example:

 fmt.Println(t1 * time.Duration(nn))

Go is not about strict maths or physics, it is about programming :blush:

Even though time.Duration has undelying int64, you can’t do operations with real int64, they are different type. Just use type conversion and everything is fine. If you convert Duration to int, multiply with another int there is no squared unit.

I believe the problem is really with the choice that Go made to make the underlying type for Duration be int64. For me, at least, when Go created a type called Duration, they set the expectation that it would have the semantics of Durations that are broadly understood outside of computing. Basing Duration in int64 makes it impossible to match those semantics. The alternative would be to define Duration as a struct with two fields: a value and a unit. Most measurement concepts can be implemented using this pattern (distances, temperatures, etc.). A downside (arguably) of using a struct is that (in Go) you can’t use the syntax of arithmetic operations for the operations on these entities — you have to write functions for each of the operations, as illustrated above by @christophberger in his ‘times’ function. This is in fact what many other languages do for Duration and other measurement concepts. The upside of the struct approach is that you don’t get yourself into the situation that Go has got itself into where the rules of the type do not match the normal semantics of the concept being modeled. In general I have been favorably impressed by Go’s design choices, but this one disappoints me. Still, for me, the best and most important choices in Go are the things they have chosen to keep out, rather than the things they have put in — I hope that continues.

Well, nanosecind is like a standard so this is why the unit is actually fixed to nanoseconds and the type is int64. All other units are abstractions over this. I like it personally, it’s easy and consistent.

const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)

I understand your point. But maybe you expect Go to be more high-level than it is. Go is more of a server language than a language for modeling scientific concepts. And Go was made simple not only for programmers but also for the compiler (resulting in incredibly short compile times). So there have to be tradeoffs somewhere.

To be fair, I think it is not the duty of the language core do accurately model every real-world concept. That’s where packages can shine. (Both the standard library and third-party packages.) The gonum project, for example, has a package for basic unit handling; this could be a start.

Granted, packages cannot redefine built-in arithmetic operators to work with custom types, but honestly, this is just syntactic sugar. When C++ was new, I was amazed by operator overloading, but today, I could not care less. In fact, I think overloaded operators can make it even more difficult to read other people’s code.

1 Like

Perhaps, but it would not have challenged the compiler to base Duration on a struct rather than int64 — that ability is already part of the language. I too am not a fan of ‘overloading’ operators — I’m fine with the normal operator symbols being reserved only for numbers and using functions for the operators of other types. I think it would have been perfectly straightforward for the Go designers to implement Duration in a way that respected the normal semantics of the concept, so I can’t help being disappointed. In any case, I’m sure it is not going to change now, so we’ll all just have to live with it.

1 Like

The rule that says it is safe to operate on two values of the same type is not a general rule for types — it only makes sense for number operators on number types. Extending your example, it makes no sense to multiply a date by a date (or a duration by a duration, which was the original example), even though they are of the same type. Although dates can’t be added or multiplied with each other, a smaller one can be subtracted from a larger one — the result is a duration, not a date. And although it does not make sense to multiply two durations, it does make sense to multiply a duration by an number (but not the other way around, perhaps). Adding 10in + 10cm is not a challenge — they are both lengths and the result is a length. There is no such type as “metric length” or “imperial length” — there are just lengths, which are measured by the time it takes light to travel that distance in a vacuum. Any length can be “stringified” in either metric or imperial notation. The problem isn’t with Go’s rules for operators on number types, the problem is that those rules are not appropriate for non-number types, so, in my view, it was a mistake to base Duration on int64. It should have been based on struct.

There have been spectacular accidents and incidents based on using the wrong unit - rockets that crashed, or the case of the “Gimli glider”. Yes, adding two values of different units can be a challenge. Wasn’t your initial complaint that Go does not handle units correctly?

Agreed, Duration could have been designed more closely to the “real world”. Again, this is not a problem of the core language and its type system but rather of the time package.

1 Like

I think we are in agreement, Christoph.

I think those examples you give (the Hubble telescope is another) are really data entry problems, not type problems. We know how to represent lengths and add them reliably, but we can’t prevent people entering 10in when they meant 10cm. It is really no different than entering 10cm when you meant 25.4cm, or saying 10 seconds when you meant 10 minutes.

Martin

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.