Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Better than turns, radians, etc. is an `Angle` newtype AKA wrapper class.

It completely eliminates misinterpretation of the value, miscalculation from [angle = angle + tau*n] as all angles are normalized, is more descriptive, and in a decent language is zero-cost.

Programmers should not be using (radians: float) in modern languages which support wrapper classes



This has nothing to do with the article, and it is equally applicable to degrees, radians, or turns. It neither solves nor hinders the simplicity or performance issues the article was talking about.


I disagree. By wrapping an angle in an Angle class, the internal representation need never be exposed to the programmer.

Rather than every programmer needing to read this blog post to see the performance benefits of using 'turns', instead now just a few library developers need to.


Types are just labels applied to variables. Their only power is type-checking a program to see whether every variable use is consistent. Wrapping something in a type doesn't magically change its value.

Not to mention, Angle is a particularly poor name, since radians, degrees and turns are all different measures of angles.

Say I have this program:

  x : Angle = 90 
  y : Angle = pi/4
  z : Angle = 1/4

  sin : Angle -> Real
  
  sin x //what will this print?
  sin y //how about this?
  sin z // ?


Why is Angle a poor name? The fact that radians, degrees and turns are all different ways to represent angles is exactly the point. Despite their different format, they represent the exact same thing, and sin(90 degrees) should return exactly the same as sin(1/4) or sin(pi/4).

From a mathematical perspective where people don't care about types, this is weird, but from an OOP perspective with polymorphism this is exactly right.

So in that sense, instead of storing angles as just a number where the programmer needs to keep checking whether it's in radians, degrees or turns, you should store it as an Angle object. And that object shouldn't use any approximations of pi, but understand what pi means. Angle.fromDegree(90) should be exactly, and not approximately Angle.fromRadian(4*PI).


You are right that `Angle` is the wrong name for a newtype but wrong that this is not a job for types.

> Wrapping something in a type doesn't magically change its value.

Most formal definitions of value disagree, e.g. from Stepanov & McJones:

A value type is a correspondence between a species (abstract or concrete) and a set of datums. A datum corresponding to a particular entity is called a representation of the entity; the entity is called the interpretation of the datum. We refer to a datum together with its interpretation as a value. In this case the species is abstract, rotational measurement - and the interpretation is the unit. 2pi radians, 360 degrees, and 1 turn all correspond to the same abstract entity; radians, degrees, and turns are types.

But you can also find the mistake just based on your own comment, where you've taken an unjustified leap from variable to value:

> Types are just labels applied to variables... Wrapping something in a type doesn't magically change its value.

Types are labels applied to variables, specifically in order to produce a particular value from the contents of some memory region (what Stepanov & McJones call "datum"). If changing types didn't change values, types would be near-useless (cf, as a concrete example, C's near-useless type system).


Parent proposed a wrapper, rather than an alias.

One way to handle this with an Angle type is to not allow an automatic cast from float/double/int. instead, you expose methods that normalise the input and internally can represent it as whatever, i.e. from_degrees, from_turns, from_radians or similar that force the programmer to be explicit about their unit, when the angle is constructed.

Alternatively, Angle could be the base class and each unit gets its own class that Angle inherits from.


> Wrapping something in a type doesn't magically change its value.

It changes its meaning and operations.

`x : Angle = 90` and friends should just be illegal. `x : Angle = Degrees(90)` is better. Then the programmer can use whatever is convenient.


> I disagree. By wrapping an angle in an Angle class, the internal representation need never be exposed to the programmer.

Because nobody will ever instantiate one?

At the end of the day, you're not implementing these as an exercise in hermetic design, you're trying to do arithmetic, presumably on numbers you have.


But what is the underlying representation in the wrapper class?


Does this matter any more than the fact the underlying representation within sin is e.g. an 80 bit number no programming language can give you? `Angle` is a bad name for this example because a newtype should be named after the unit rather than what it models, but once you've newtyped it, what matters is that you call sin with the right one, not the actual representation.


Yes, the entire point of the article is that it does in fact matter.

> what matters is that you call sin with the right one, not the actual representation.

No, the actual representation does matter. At some point, arithmetic operations are being performed and those operations consume CPU cycles. If you care about the performance of the code, you care about having a representation that minimizes the number of those operations.


So make sure you use `typeof sin[0]` or whatever the syntax is in your language for your own variables - in the end this still requires newtyping the thing.

For performance in this context, the actual representation does not matter - only that your code and your sin code agree to avoid the conversion cost.


I think you're confusing "representation" to mean just the number of bits allocated to the number, but the article and my comments also use it to mean what the numeric range of those values represents (hence "representation").

How that's modeled in the static type system is completely orthogonal to the underlying arithmetic operations that are performed and their efficiency. The article is entirely about the latter.


No, I think you're confused about what I'm saying about performance. I definitely don't think "representation" just means the number of bits.

> How that's modeled in the static type system is completely orthogonal to the underlying arithmetic operations that are performed and their efficiency.

No! A type system gives information in in both directions. Normally yes, we use it to impose some interpretation on a representation to produce a value, so we can think in values and not bits. But if two types model the same range of entities, i.e. angular measurements, it also tells you if you have a value with a representation that you might have to spend time futzing with to integrate with something else expecting the same entity but a different type.

If we had reasonable types, sin could define the type it wants to do what it does efficiently (turns, or radians if the platform has an efficient instruction, or a lookup table index into some division of a quadrant, or whatever). That representation matters to the sin author, but not the user. All the user cares about is, then, how many times do I have to pay some conversion cost? And if I'm using the type the sin author provided, the answer is zero (or maybe like one, at initial load). But that relies on the sin author using such a newtype.

(Arguably the user also cares how much memory it takes to store the representation the sin author chose, but in practice for sin that's going to be a word or less whatever the representation is.)

You also have a blinkered view of the article if you think it's "entirely about" performance, given how often it also talks about simplicity and accuracy.


I feel like we're talking past each other or the goal posts are moving or something.

> If we had reasonable types, sin could define the type it wants to do what it does efficiently (turns, or radians if the platform has an efficient instruction, or a lookup table index into some division of a quadrant, or whatever). That representation matters to the sin author, but not the user. All the user cares about is, then, how many times do I have to pay some conversion cost? And if I'm using the type the sin author provided, the answer is zero (or maybe like one, at initial load). But that relies on the sin author using such a newtype.

Everything here is equally true if you take types entirely out. sin() will ultimately be doing math on floating point values. The sin() function expects angles according to a specified scale. Some scales (turns, according to the author) require one less multiplication in the function prelude. Therefore, sin() is faster if it specifies that as the input scale.

That benefit is negated if angles are stored in another scale and rescaled every time sin() is called. That's just hoisting the multiplication out to every callsite.

So what you need is to store angles in the numeric scale that is fastest for the implementation of sin().

Whether those floating point angles are wrapped in a newtype is completely orthogonal. What matters is that the numeric scale you use to store and represent angles is the one that requires the least arithmetic in the underlying implementation of the trig functions.

Unit types are great, but they solve different problems than the article is talking about.


you can have an internal representation enum and when calling

sin(Angle a){ switch a.representation: rad : sin(a.rad); turn: nsin(a.turn) }

conversion only needed when adding angles of different representation.


Sounds like you're balancing out the performance gains the article talks about with performance losses from branching.


Now your performance is even worse.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: