#601 Measurement APIs

qualidafial Wed 20 May 2009

I work in the cabinet industry. In our code we make heavy use of size-centric API--I'm not sure if this would be generally useful but I figured I'd show you how it works. In Fan these would look like:

width := Dist.inch(30)
leftSpacing := Dist.inch(0.0625)
rightSpacing := Dist.inch(0.0625)
doorSpacing := Dist.inch(0.125)
doorCount := 2

doorWidth := (width - leftSpacing - rightSpacing -
    doorSpacing * (doorCount-1)) / doorCount
echo(doorWidth)       => "14 7/8\""
echo(doorWidth.val) => 14.875
echo(doorWidth.unit)  => DistUnit.inch

What I like in particular about this approach is how the method names make the code very intentional: Dist.mm is a measurement of distance expressed in millimeters; Dist.inch is a measurement of distance expressed in inches.

I've been thinking about how this could be modeled more generically into a Measure mixin:

mixin Unit
{
  Decimal ratio(This other)
}

mixin Measure
{
  Decimal val()
  Unit unit()
  This convert(Unit unit)

  // Can't believe I remember these terms from elementary school..
  This plus(This addend)
  This minus(This subtrahend)
  This mult(Decimal multiplicand) 
  This div(Decimal divisor)
  // etc for other math functions..
}

Thus:

enum DistUnit
{
  mi, yd, ft, inch, km, m, cm, mm, nm

  // ratio(DistUnit) omitted for brevity
}

class Dist : Measure
{
  new mi(Decimal val)
  new yd(Decimal val)
  new ft(Decimal val)
  new inch(Decimal val)
  new km(Decimal val)
  new m(Decimal val)
  new cm(Decimal val)
  new mm(Decimal val)
  new nm(Decimal val)
  new make(Decimal val, DistUnit unit)

  DistUnit unit() // covary return type by concrete class
}

This same concept could be applied to other measurements, e.g:

enum MassUnit
{
  kg, g, mg
}

class Mass : Measure
{
  new kg(Decimal val)
  new g(Decimal val)
  new mg(Decimal val)
  new make(Decimal val, MassUnit unit)

  MassUnit unit() // covary return type by concrete class
}

Maybe some more specialized APIs like this would be more humane than the current sys::Unit class. :)

brian Wed 20 May 2009

There is a difference between modeling units and quantities that have a unit. The unit API is a database of units so that higher layers can agree what things like "meter" actually mean.

When it comes to quantities, what happens if I want Ints or Floats? Or maybe a list of Int, Float, or Decimal? Would I create create a wrapper class for every dimension with methods for every unit? I participated in the units JSR which tried to model units with the type system, and I personally didn't really like the result.

So my only goal is to define a normalized representation for what a Unit is so that higher levels can share. How you attach that as meta-data to the values is an application layer issue.

jodastephen Thu 21 May 2009

This is a topic that has come up a lot, so we shouldn't see it as solved yet.

Personally, I'd like to see a new type of literal for units and quantities. I haven't worked out the details, but I think:

// user codes
3_miles
6_metres
10.5_years

// I'm thinking these are mapped to calls to a constructor
Distance.miles(3)
Length.metres(6)
Duration.years(10.5)

// plus we'd probably need a using clause
using literal Distance

As I say, its and outline concept. The tricky part is the using statement (or similar) that causes the compiler to allow specific literal suffixes.

The nice part is that it removes the need for dedicated Duration literals. So, its a unification proposal.

qualidafial Thu 21 May 2009

When it comes to quantities, what happens if I want Ints or Floats?

We could change all the sys::Decimal fields and method args to sys::Num if that makes a difference.

Would I create create a wrapper class for every dimension with methods for every unit?

I assume you're talking about object overhead? In practice this has never been a problem, and the ability to combine measurements instead of numbers has simplified things like adding measurements of different units:

a := Dist.inch(1)
b := Dist.mm(25.4)
c := a+b
echo(c)            => 2"

My only complaint so far has been Java doesn't allow operator overloading, so things like a*b+c have to be coded as a.mult(b).plus(c) which is noisy (and a little annoying).

I participated in the units JSR which tried to model units with the type system, and I personally didn't really like the result.

Which JSR?

// plus we'd probably need a using clause using literal Distance

You probably also want a literal keyword or annotation on just the constructors that make sense as literals, so that ctors like make doesn't get dragged into the mix:

new literal cm(Num val)          // as keyword
@literal new mm(Num val)         // as annotation
new make(Num val, DistUnit unit) // not a literal

On another note, I'm wondering if the compiler is going to treat these two expressions the same:

Dist a := Dist.inch(1)
Decimal b := 2
Dist c := a*b // this compiles, but
Dist d := b*a // does this?

For commutative math functions like * it may make sense for the compiler to try the operands in the opposite order if the second operand doesn't fit the method argument type for the first operand.

brian Thu 21 May 2009

I assume you're talking about object overhead?

I was more talking about the 60 different quantities the unit database has already. But object overhead has an impact too (especially if you box to Num). In my field, units are almost always attached as meta-data and you never code with a specific unit directly (time aside). Plus I typically work with millions, if not billions of points of data, and overhead is pretty important to me. This is why it is common with tabular data to attach the unit meta-data to the column, not each individual value.

I think the main point here, is that how units are used is very much an application layer issue. I am not sure there is a good generic solution, in the mean-time my only goal is to created a normalized representation for the definition of a unit (which is actually immensely useful regardless of how the higher layers are implemented).

Which JSR

JSR 275

For commutative math functions like * it may make sense for the compiler to try the operands in the opposite order if the second operand doesn't fit the method argument type for the first operand

The compiler does not do this today. It seems reasonable for + or *, although it definitely would muddy the elegance of operators mapping directly to methods.

qualidafial Thu 21 May 2009

The lack of slot name overloading also makes things like this impossible:

Dist distance := Dist.mi(60)
Dist halfway := distance / 2  => 30mi
Speed speed := distance / 2hr => 30mi/hr

You cannot have both because slot name overloading is not allowed, unless there is some other way to make a method into a math function. Maybe facets can help us here:

class Dist {
       Dist  div        (Num      dividend)
  @div Speed divDuration(Duration duration)
  @div Num   divDist    (Dist     dividend)
}

KevinKelley Thu 21 May 2009

This units thing sounds like a really good first test for the DSL proposal. Rather than trying to forcefit ring algebras and all that whatnot into the base language, write a DSL that understands all the proper associativities, with its own syntax; have a language feature for communicating back and forth to it.

Core Fan shouldn't be trying to provide syntax for everything from furlongs to string theory; but if that DSL thing works right it can cleanly do all that anyway.

brian Thu 21 May 2009

The lack of slot name overloading also makes things like this impossible

It isn't impossible, you just have to type your method using Obj and give up static type checking.

tompalmer Thu 21 May 2009

It isn't impossible, you just have to type your method using Obj and give up static type checking.

I still think it might be good to allow operator overloading without slot name overloading. For example, allow * to map to anything starting with mult (if the next letter isn't a lowercase letter?), and apply normal overloading rules from Java/C++ at that point. Or maybe a variation on that theme.

As I've mentioned before, this (operator overloading) is already supported in Fan for special cases: get and slice, and make and fromStr.

brian Thu 21 May 2009

still think it might be good to allow operator overloading without slot name overloading. For example, allow * to map to anything starting with mult (if the next letter isn't a lowercase letter?),

I like that, although I would formalize the naming rule to be something like shortcut+typename:

Duration DateTime.minus(DateTime)
DateTime DateTime.minusDuration(Duration)

tompalmer Thu 21 May 2009

I would formalize the naming rule to be something like shortcut+typename

I've thought the same, but I was wondering how pod names would play in. Just leave them out? The chance of needing to overload to two types with the same name from different pods seems slim. And if you really needed it, maybe you'd just need to work around by using standard calling conventions rather than operators? That might cover things well enough.

qualidafial Thu 21 May 2009

> I would formalize the naming rule to be something like shortcut+typename I've thought the same, but I was wondering how pod names would play in.

I was wondering about pod names too. Using facets would remove ambiguity, I think:

class DateTime
{
  @minus Duration minus(DateTime)
  @minus DateTime minusDuration(Duration)
}

Using facets the compiler magic could be explicit instead of intrinsic in the naming convention:

class Dist
{
  @div Dist  div        (Num divisor)
  @div Num   divDist    (Dist divisor)
  @div Speed divDuration(Duration duration)
}

class Speed
{
  @mult Speed mult        (Num factor)
  @mult Dist  multDuration(Duration duration)
}

speed := Dist.km(30) / 1hr => 30km/hr
distance := speed * 1.5hr    => 45km

class DateTime
{
  @minus DateTime minus(Duration duration)
  @minus Duration minusDateTime(DateTime dateTime)
}

now := DateTime.now
tomorrow := now.plus(1day)
interval := tomorrow - now  => 1day

> For commutative math functions like * it may make sense for the compiler to try the operands in the opposite order if the second operand doesn't fit the method argument type for the first operand

The compiler does not do this today. It seems reasonable for + or * although it definitely would muddy the elegance of operators mapping directly to methods.

I agree, only + and * would make sense for this. What if we used a facet so classes can opt in to this behavior case by case?

class Dist
{
  @mult @commutative Dist mult(Num multiplicand)
}

class DateTime
{
  @plus @commutative DateTime plus(Duration duration)
}

class Duration
{
  @mult @commutative Duration mult(Num multiplicand)
}

qualidafial Thu 21 May 2009

I'm going to go ahead and start a little project on bitbucket for this. I'll be naming the pod measure for lack of a better idea. (suggestions welcome)

I will need a few things from Fan to make these work elegantly:

  • Overloading operators either by naming convension (e.g. div vs divDuration) or by facets (e.g. @mult, @add).
  • Commutative addition and multiplication on + and * expressions when the first operand doesn't know how to add the second operand, but the second knows how to add the first. A facet seems the right approach to me but I'll take anything that works.
  • sys::Duration literals are converted to nanoseconds, and do not remember the unit of time used to define them. It would be useful to save that unit of time so it can be queried and referenced directly in code. e.g. a unit of speed is expressed as a unit of distance divided by a unit of time. Right now there is no enum in sys to represent a unit of time so if you guys do not want to support this natively in Fan I will probably have to duplicate Duration to achieve it.
  • A literal syntax as advocated by Stephen would be really great.

brian Thu 21 May 2009

Overloading operators either by naming convension (e.g. div vs divDuration) or by facets (e.g. @mult, @add).

I am for this feature, although I prefer the explicit naming by type - this is a case where the convention should be the rule. What we've to hack around it in DateTime and Duration is ugly.

Commutative addition and multiplication on + and * expressions when the first operand doesn't know how to add the second operand,

I am not ready to support this feature, at the very least I need to think it about it a while. Although, since it won't ever work for - or /, I am not sure it really matters, the lhs will always have to be the type further down the dependency chain.

sys::Duration literals are converted to nanoseconds, and do not remember the unit of time used to define them. It would be useful to save that unit of time so it can be queried and referenced directly in code

It doesn't remember, in practice is hardly ever matters because things get rounded to the whole unit:

fansh> 1sec.toStr
1sec
fansh> 1hr.toStr
1hr

A literal syntax as advocated by Stephen would be really great.

I don't think this belongs on the radar for 1.0

jodastephen Thu 21 May 2009

sys::Duration literals are converted to nanoseconds, and do not remember the unit of time used to define them.

And unfortunately this can be wrong when correctly evaluating dates and times. 1 minute is not necessarily the same as 60 seconds (leap seconds). Similarly for 1 day is not necessarily 24 hours (daylight savings).

As such, have a literal form that creates a Period (Joda-Time/JSR-310 terminology) that does know and remember what you created is a useful thing.

tompalmer Thu 21 May 2009

1 minute is not necessarily the same as 60 seconds (leap seconds). Similarly for 1 day is not necessarily 24 hours (daylight savings).

Joda-Time isn't always as easy to work with as I'd like (sorry -- though I still like it much better than java.util.Calendar of course), but it teaches well the idea of using semantic terminology at the human level, when the human level is what matters (which I find to be often the case).

jodastephen Thu 21 May 2009

I'd love to hear why and what difficulties you've faced with Joda-Time so I don't repeat the mistakes! But lets discuss it privately or on the Joda list, not here!

qualidafial Thu 21 May 2009

I've posted an initial measure pod to bitbucket:

http://bitbucket.org/qualidafial/fan-measure/

So far there's just Dist and DistUnit, I'll add the other units but this is a decent start.

tompalmer Fri 22 May 2009

But lets discuss it privately or on the Joda list, not here!

Sorry. Bad form on my part. Problem will be that it's been a year since I last used it. If I get a chance to take a look again, I'll see if I can provide meaningful feedback in a more appropriate venue.

My main point here (lost in my poorly chosen comment) was definite agreement on the importance of being able to deal with time in terms of the units we use in human language.

Login or Signup to reply.